Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 66 additions & 35 deletions examples/authentication/Identity/Persister.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,68 +46,99 @@ public function select(object $transaction, mixed $criteria): iterable
}
}

public function insert(object $transaction, object $entity): void
public function persist(object $transaction, ORM\Changes $changes): void
{
try {
$transaction->execute(
<<<'SQL'
insert into identity (id, password_hash)
values (?, ?)
SQL,
[
$entity->id->toString(),
$entity->passwordHash,
],
);
} catch (PostgresQueryError $error) {
if (str_contains(strtolower($error->getMessage()), 'duplicate key value violates unique constraint')) {
throw new Exception\DuplicateEntity(previous: $error);
}
$this->insert($transaction, $changes->inserts);
$this->update($transaction, $changes->updates);
$this->delete($transaction, $changes->deletes);
}

/**
* @param list<Identity> $entities
*/
private function insert(PostgresLink $transaction, array $entities): void
{
if ($entities === []) {
return;
}

$statement = $transaction->prepare(
<<<'SQL'
insert into identity (id, password_hash)
values (?, ?)
SQL,
);

foreach ($entities as $entity) {
try {
$statement->execute([$entity->id->toString(), $entity->passwordHash]);
} catch (PostgresQueryError $error) {
if (str_contains(strtolower($error->getMessage()), 'duplicate key value violates unique constraint')) {
throw new Exception\DuplicateEntity(previous: $error);
}

throw $error;
throw $error;
}
}
}

public function update(object $transaction, object $entity, object $snapshot): void
/**
* @param list<ORM\Update<Identity>> $updates
*/
private function update(PostgresLink $transaction, array $updates): void
{
if ($entity->passwordHash === $snapshot->passwordHash) {
if ($updates === []) {
return;
}

$result = $transaction->execute(
$statement = $transaction->prepare(
<<<'SQL'
update identity
set password_hash = ?,
version = version + 1
where id = ? and version = ?
SQL,
[
$entity->passwordHash,
$entity->id->toString(),
$entity->version,
],
);

if ($result->getRowCount() !== 1) {
throw new Exception\ConcurrentModification();
foreach ($updates as $update) {
if ($update->entity->passwordHash === $update->snapshot->passwordHash) {
continue;
}

$result = $statement->execute([
$update->entity->passwordHash,
$update->entity->id->toString(),
$update->entity->version,
]);

if ($result->getRowCount() !== 1) {
throw new Exception\ConcurrentModification();
}
}
}

public function delete(object $transaction, object $entity): void
/**
* @param list<Identity> $entities
*/
private function delete(PostgresLink $transaction, array $entities): void
{
$result = $transaction->execute(
if ($entities === []) {
return;
}

$statement = $transaction->prepare(
<<<'SQL'
delete from identity
where id = ? and version = ?
SQL,
[
$entity->id->toString(),
$entity->version,
],
);

if ($result->getRowCount() !== 1) {
throw new Exception\ConcurrentModification();
foreach ($entities as $entity) {
$result = $statement->execute([$entity->id->toString(), $entity->version]);

if ($result->getRowCount() !== 1) {
throw new Exception\ConcurrentModification();
}
}
}
}
37 changes: 37 additions & 0 deletions src/Changes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Thesis\ORM;

/**
* @api
* @template TEntity of object
*/
final readonly class Changes
{
/**
* @template MEntity of object
* @param list<self<MEntity>> $changes
* @return self<MEntity>
*/
public static function merge(array $changes): self
{
return new self(
inserts: array_merge(...array_column($changes, 'inserts')),
updates: array_merge(...array_column($changes, 'updates')),
deletes: array_merge(...array_column($changes, 'deletes')),
);
}

/**
* @param list<TEntity> $inserts
* @param list<Update<TEntity>> $updates
* @param list<TEntity> $deletes
*/
public function __construct(
public array $inserts = [],
public array $updates = [],
public array $deletes = [],
) {}
}
28 changes: 18 additions & 10 deletions src/Internal/ExistingEntity.php → src/Internal/Existing.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@

namespace Thesis\ORM\Internal;

use Thesis\ORM\Changes;
use Thesis\ORM\Exception\DuplicateEntity;
use Thesis\ORM\Exception\EntityNotManaged;
use Thesis\ORM\Persister;
use Thesis\ORM\Update;

/**
* @internal
*
* @template TTransaction of object
* @template TEntity of object
* @implements ManagedEntity<TTransaction, TEntity>
*/
final class ExistingEntity implements ManagedEntity
final class Existing
{
private bool $remove = false;

Expand All @@ -25,16 +24,18 @@ final class ExistingEntity implements ManagedEntity
private readonly object $snapshot;

/**
* @param Persister<TTransaction, TEntity, *> $persister
* @param TEntity $entity
*/
public function __construct(
private readonly Persister $persister,
public readonly object $entity,
) {
$this->snapshot = clone $entity;
}

/**
* @param TEntity $entity
* @throws DuplicateEntity
*/
public function add(object $entity): void
{
if ($entity !== $this->entity) {
Expand All @@ -44,6 +45,10 @@ public function add(object $entity): void
$this->remove = false;
}

/**
* @param TEntity $entity
* @throws EntityNotManaged
*/
public function remove(object $entity): void
{
if ($entity !== $this->entity) {
Expand All @@ -53,12 +58,15 @@ public function remove(object $entity): void
$this->remove = true;
}

public function flush(object $transaction): void
/**
* @return Changes<TEntity>
*/
public function collectChanges(): Changes
{
if ($this->remove) {
$this->persister->delete($transaction, $this->entity);
} else {
$this->persister->update($transaction, $this->entity, $this->snapshot);
return new Changes(deletes: [$this->entity]);
}

return new Changes(updates: [new Update($this->entity, $this->snapshot)]);
}
}
48 changes: 38 additions & 10 deletions src/Internal/ManagedEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,66 @@

namespace Thesis\ORM\Internal;

use Thesis\ORM\Exception\ConcurrentModification;
use Thesis\ORM\Changes;
use Thesis\ORM\Exception\DuplicateEntity;
use Thesis\ORM\Exception\EntityNotManaged;

/**
* @internal
*
* @template TTransaction of object
* @template TEntity of object
*/
interface ManagedEntity
final class ManagedEntity
{
/**
* @var ?TEntity
* @param Existing<TEntity>|NonExisting<TEntity> $state
*/
public ?object $entity { get; }
public function __construct(
private Existing|NonExisting $state,
) {}

/**
* @param TEntity $entity
* @return TEntity
*/
public function resolveFound(object $entity): object
{
if ($this->state instanceof Existing) {
return $this->state->entity;
}

if ($this->state->entity !== null) {
$entity = $this->state->entity;
$this->state = new Existing($entity);

return $entity;
}

$this->state = new Existing($entity);

return $entity;
}

/**
* @param TEntity $entity
* @throws DuplicateEntity
*/
public function add(object $entity): void;
public function add(object $entity): void
{
$this->state->add($entity);
}

/**
* @param TEntity $entity
* @throws EntityNotManaged
*/
public function remove(object $entity): void;
public function remove(object $entity): void
{
$this->state->remove($entity);
}

/**
* @param TTransaction $transaction
* @throws DuplicateEntity|ConcurrentModification
* @var Changes<TEntity>
*/
public function flush(object $transaction): void;
public Changes $changes { get => $this->state->collectChanges(); }
}
45 changes: 45 additions & 0 deletions src/Internal/ManagedPersister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Thesis\ORM\Internal;

use Thesis\ORM\Changes;
use Thesis\ORM\Persister;

/**
* @internal
*
* @template TTransaction of object
* @template TEntity of object
*/
final class ManagedPersister
{
/**
* @var list<ManagedEntity<TEntity>>
*/
private array $entities = [];

/**
* @param Persister<TTransaction, TEntity, *> $persister
*/
public function __construct(
private readonly Persister $persister,
) {}

/**
* @param ManagedEntity<TEntity> $entity
*/
public function addEntity(ManagedEntity $entity): void
{
$this->entities[] = $entity;
}

/**
* @param TTransaction $transaction
*/
public function persist(object $transaction): void
{
$this->persister->persist($transaction, Changes::merge(array_column($this->entities, 'changes')));
}
}
Loading