Pairity/src/DAO/BaseDAO.php
Funky Waddle 61cefca113
Some checks failed
CI / test (8.2) (push) Has been cancelled
CI / test (8.3) (push) Has been cancelled
CI / test (8.4) (push) Has been cancelled
Fix issues with missing classes when being tested in Github Actions
2026-02-08 02:48:39 -06:00

319 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Pairity\DAO;
use Pairity\Contracts\Database\DatabaseManagerInterface;
use Pairity\DTO\IdentityMap;
/**
* Class BaseDAO
*
* Base class for all generated DAOs.
*
* @package Pairity\DAO
*/
abstract class BaseDAO
{
/**
* @var string The table name.
*/
protected string $table;
/**
* @var string The primary key.
*/
protected string $primaryKey = 'id';
/**
* @var string The connection name.
*/
protected string $connection = 'default';
/**
* @var array<string, mixed>
*/
protected array $options = [];
/**
* @var \Pairity\Contracts\Schema\HydratorInterface|null
*/
protected ?\Pairity\Contracts\Schema\HydratorInterface $hydrator = null;
/**
* @var string|null
*/
protected ?string $dtoClass = null;
/**
* @var string|null
*/
protected ?string $lockingColumn = null;
/**
* @var bool
*/
protected bool $auditable = false;
/**
* BaseDAO constructor.
*
* @param DatabaseManagerInterface $db
* @param IdentityMap $identityMap
*/
public function __construct(
protected DatabaseManagerInterface $db,
protected IdentityMap $identityMap
) {
}
/**
* Set the hydrator for the DAO.
*
* @param \Pairity\Contracts\Schema\HydratorInterface $hydrator
* @return void
*/
public function setHydrator(\Pairity\Contracts\Schema\HydratorInterface $hydrator): void
{
$this->hydrator = $hydrator;
}
/**
* Get the connection instance.
*
* @return \Pairity\Contracts\Database\ConnectionInterface
*/
protected function getConnection(): \Pairity\Contracts\Database\ConnectionInterface
{
return $this->db->connection($this->connection);
}
/**
* Create a new query builder for the table.
*
* @return \Pairity\Database\Query\Builder
*/
public function query(): \Pairity\Database\Query\Builder
{
$connection = $this->getConnection();
$grammar = $this->db->getQueryGrammar($connection->getDriver()->getName());
$builder = new \Pairity\Database\Query\Builder($this->db, $connection, $grammar);
if ($this->dtoClass) {
$builder->setModel($this->dtoClass, $this);
}
return $builder->from($this->table);
}
/**
* @return string
*/
public function getTable(): string
{
return $this->table;
}
/**
* Get the connection name.
*
* @return string
*/
public function getConnectionName(): string
{
return $this->connection;
}
/**
* @return string|null
*/
public function getDtoClass(): ?string
{
return $this->dtoClass;
}
/**
* @return string
*/
public function getPrimaryKey(): string
{
return $this->primaryKey;
}
/**
* Get a DAO option.
*
* @param string $key
* @param mixed|null $default
* @return mixed
*/
public function getOption(string $key, mixed $default = null): mixed
{
return $this->options[$key] ?? $default;
}
/**
* @return \Pairity\Contracts\Schema\HydratorInterface|null
*/
public function getHydrator(): ?\Pairity\Contracts\Schema\HydratorInterface
{
return $this->hydrator;
}
/**
* @return IdentityMap
*/
public function getIdentityMap(): IdentityMap
{
return $this->identityMap;
}
/**
* Delete a record by its primary key.
*
* @param mixed $id
* @return int
*/
public function delete(mixed $id): int
{
return $this->getConnection()
->execute("DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?", [$id]);
}
/**
* Fire the given event for the model.
*
* @param string $event
* @param mixed $payload
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent(string $event, mixed $payload = null, bool $halt = true): mixed
{
$dispatcher = $this->db->getDispatcher();
$fullEventName = "pairity.model.{$event}: " . get_class($payload ?? $this);
return $dispatcher->dispatch($fullEventName, $payload, $halt);
}
/**
* Get the database manager.
*
* @return DatabaseManagerInterface
*/
public function getDb(): DatabaseManagerInterface
{
return $this->db;
}
/**
* Save a DTO instance.
*
* @param \Pairity\DTO\BaseDTO $dto
* @return bool
* @throws \Pairity\Exceptions\DatabaseException
*/
public function save(\Pairity\DTO\BaseDTO $dto): bool
{
$data = $dto->toArray();
$primaryKeyValue = $data[$this->primaryKey] ?? null;
if ($primaryKeyValue === null) {
return $this->insert($data);
}
return $this->update($primaryKeyValue, $data, $dto);
}
/**
* Insert a new record.
*
* @param array<string, mixed> $data
* @param \Pairity\DTO\BaseDTO|null $dto
* @return bool
*/
protected function insert(array $data, ?\Pairity\DTO\BaseDTO $dto = null): bool
{
if ($this->getOption('tenancy', false)) {
$tenantColumn = \Pairity\Database\Query\Scopes\TenantScope::getTenantColumn();
if (!isset($data[$tenantColumn]) && ($tenantId = \Pairity\Database\Query\Scopes\TenantScope::getTenantId())) {
$data[$tenantColumn] = $tenantId;
}
}
if ($this->lockingColumn && !isset($data[$this->lockingColumn])) {
$data[$this->lockingColumn] = 1;
}
$grammar = $this->db->getQueryGrammar($this->getConnection()->getDriver()->getName());
$columns = implode(', ', array_map([$grammar, 'wrap'], array_keys($data)));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
return $this->getConnection()->execute($sql, array_values($data)) > 0;
}
/**
* Update an existing record.
*
* @param mixed $id
* @param array<string, mixed> $data
* @param \Pairity\DTO\BaseDTO|null $dto
* @return bool
* @throws \Pairity\Exceptions\DatabaseException
*/
protected function update(mixed $id, array $data, ?\Pairity\DTO\BaseDTO $dto = null): bool
{
$sets = [];
$values = [];
$grammar = $this->db->getQueryGrammar($this->getConnection()->getDriver()->getName());
$currentVersion = null;
if ($this->lockingColumn) {
$currentVersion = $data[$this->lockingColumn] ?? null;
$data[$this->lockingColumn] = ($currentVersion ?: 1) + 1;
}
foreach ($data as $column => $value) {
if ($column === $this->primaryKey) continue;
$sets[] = $grammar->wrap($column) . " = ?";
$values[] = $value;
}
$values[] = $id;
$sql = "UPDATE " . $grammar->wrapTable($this->table) . " SET " . implode(', ', $sets) . " WHERE " . $grammar->wrap($this->primaryKey) . " = ?";
if ($this->lockingColumn && $currentVersion !== null) {
$sql .= " AND " . $grammar->wrap($this->lockingColumn) . " = ?";
$values[] = $currentVersion;
}
$affected = $this->getConnection()->execute($sql, $values);
if ($this->lockingColumn && $affected === 0) {
throw new \Pairity\Exceptions\DatabaseException("Optimistic locking failed for table [{$this->table}] and ID [{$id}].");
}
if ($dto && $this->lockingColumn) {
$dto->setAttribute($this->lockingColumn, $data[$this->lockingColumn]);
}
return $affected > 0;
}
/**
* Update or insert records.
*
* @param array $values
* @param array $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, array $uniqueBy, array $update = null): int
{
return $this->query()->upsert($values, $uniqueBy, $update);
}
}