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
83 changes: 67 additions & 16 deletions src/AbstractMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use function array_flip;
use function array_intersect_key;
use function assert;
use function count;
use function is_int;
use function is_scalar;
Expand All @@ -23,7 +24,7 @@ abstract class AbstractMapper
/** @var SplObjectStorage<object, string> Maps entity → 'insert'|'update'|'delete' */
protected SplObjectStorage $pending;

/** @var array<string, array<int|string, object>> PK-indexed identity map: [collectionName][pkValue] → entity */
/** @var array<string, array<int|string, object>> Identity-indexed map: [collectionName][idValue] → entity */
protected array $identityMap = [];

/** @var array<string, Collection> */
Expand Down Expand Up @@ -77,13 +78,13 @@ public function markTracked(object $entity, Collection $collection): bool
return true;
}

public function persist(object $object, Collection $onCollection): bool
public function persist(object $object, Collection $onCollection): object
{
$next = $onCollection->next;
if ($onCollection instanceof Filtered && $next !== null) {
$this->persist($object, $next);

return true;
return $object;
}

if ($this->isTracked($object)) {
Expand All @@ -92,13 +93,17 @@ public function persist(object $object, Collection $onCollection): bool
$this->pending[$object] = 'update';
}

return true;
return $object;
}

if ($onCollection->name !== null && $this->tryReplaceFromIdentityMap($object, $onCollection)) {
return $object;
}

$this->pending[$object] = 'insert';
$this->markTracked($object, $onCollection);

return true;
return $object;
}

public function remove(object $object, Collection $fromCollection): bool
Expand All @@ -117,6 +122,18 @@ public function isTracked(object $entity): bool
return $this->tracked->offsetExists($entity);
}

public function replaceTracked(object $old, object $new, Collection $onCollection): void
{
$op = $this->pending[$old] ?? 'update';
$this->tracked->offsetUnset($old);
$this->pending->offsetUnset($old);
$this->evictFromIdentityMap($old, $onCollection);

$this->markTracked($new, $onCollection);
$this->registerInIdentityMap($new, $onCollection);
$this->pending[$new] = $op;
}

public function registerCollection(string $alias, Collection $collection): void
{
$collection->mapper = $this;
Expand All @@ -141,9 +158,9 @@ protected function filterColumns(array $columns, Collection $collection): array
return $columns;
}

$pk = $this->style->identifier($collection->name);
$id = $this->style->identifier($collection->name);

return array_intersect_key($columns, array_flip([...$collection->filters, $pk]));
return array_intersect_key($columns, array_flip([...$collection->filters, $id]));
}

protected function resolveHydrator(Collection $collection): Hydrator
Expand All @@ -157,12 +174,12 @@ protected function registerInIdentityMap(object $entity, Collection $coll): void
return;
}

$pkValue = $this->entityPkValue($entity, $coll->name);
if ($pkValue === null) {
$idValue = $this->entityIdValue($entity, $coll->name);
if ($idValue === null) {
return;
}

$this->identityMap[$coll->name][$pkValue] = $entity;
$this->identityMap[$coll->name][$idValue] = $entity;
}

protected function evictFromIdentityMap(object $entity, Collection $coll): void
Expand All @@ -171,12 +188,12 @@ protected function evictFromIdentityMap(object $entity, Collection $coll): void
return;
}

$pkValue = $this->entityPkValue($entity, $coll->name);
if ($pkValue === null) {
$idValue = $this->entityIdValue($entity, $coll->name);
if ($idValue === null) {
return;
}

unset($this->identityMap[$coll->name][$pkValue]);
unset($this->identityMap[$coll->name][$idValue]);
}

protected function findInIdentityMap(Collection $collection): object|null
Expand All @@ -193,11 +210,45 @@ protected function findInIdentityMap(Collection $collection): object|null
return $this->identityMap[$collection->name][$condition] ?? null;
}

private function entityPkValue(object $entity, string $collName): int|string|null
private function tryReplaceFromIdentityMap(object $entity, Collection $coll): bool
{
assert($coll->name !== null);
$entityId = $this->entityIdValue($entity, $coll->name);
$idValue = $entityId;

if ($idValue === null && is_scalar($coll->condition)) {
$idValue = $coll->condition;
}

if ($idValue === null || (!is_int($idValue) && !is_string($idValue))) {
return false;
}

$existing = $this->identityMap[$coll->name][$idValue] ?? null;
if ($existing === null || $existing === $entity) {
return false;
}

if ($entityId === null) {
$idName = $this->style->identifier($coll->name);
$this->entityFactory->set($entity, $idName, $idValue);
}

$this->tracked->offsetUnset($existing);
$this->pending->offsetUnset($existing);
$this->evictFromIdentityMap($existing, $coll);
$this->markTracked($entity, $coll);
$this->registerInIdentityMap($entity, $coll);
$this->pending[$entity] = 'update';

return true;
}

private function entityIdValue(object $entity, string $collName): int|string|null
{
$pkValue = $this->entityFactory->get($entity, $this->style->identifier($collName));
$idValue = $this->entityFactory->get($entity, $this->style->identifier($collName));

return is_int($pkValue) || is_string($pkValue) ? $pkValue : null;
return is_int($idValue) || is_string($idValue) ? $idValue : null;
}

public function __get(string $name): Collection
Expand Down
26 changes: 17 additions & 9 deletions src/Collections/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use ArrayAccess;
use Respect\Data\AbstractMapper;
use Respect\Data\EntityFactory;
use Respect\Data\Hydrator;
use RuntimeException;

Expand Down Expand Up @@ -51,9 +50,24 @@ public function addChild(Collection $child): void
$this->children[] = $clone;
}

public function persist(object $object): bool
public function persist(object $object, mixed ...$changes): object
{
return $this->resolveMapper()->persist($object, $this);
$mapper = $this->resolveMapper();

if ($changes) {
$original = $object;
$object = $mapper->entityFactory->withChanges($original, ...$changes);

if ($mapper->isTracked($original)) {
$mapper->replaceTracked($original, $object, $this);

return $object;
}
}

$mapper->persist($object, $this);

return $object;
}

public function remove(object $object): bool
Expand All @@ -71,12 +85,6 @@ public function fetchAll(mixed $extra = null): mixed
return $this->resolveMapper()->fetchAll($this, $extra);
}

/** @param object|array<string, mixed> $row */
public function resolveEntityName(EntityFactory $factory, object|array $row): string
{
return $this->name ?? '';
}

public function offsetExists(mixed $offset): bool
{
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/Collections/Filtered.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

final class Filtered extends Collection
{
/** Fetch only the entity identifier (primary key, document ID, etc.) */
/** Fetch only the entity identifier */
public const string IDENTIFIER_ONLY = '*';

// phpcs:ignore PSR2.Classes.PropertyDeclaration
Expand Down
10 changes: 7 additions & 3 deletions src/Collections/Typed.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ public function __construct(
parent::__construct($name);
}

/** @param object|array<string, mixed> $row */
public function resolveEntityName(EntityFactory $factory, object|array $row): string
/**
* @param object|array<string, mixed> $row
*
* @return class-string
*/
public function resolveEntityClass(EntityFactory $factory, object|array $row): string
{
$name = is_array($row) ? ($row[$this->type] ?? null) : $factory->get($row, $this->type);

return is_string($name) ? $name : ($this->name ?? '');
return $factory->resolveClass(is_string($name) ? $name : (string) $this->name);
}

/** @param array<int, string> $arguments */
Expand Down
Loading
Loading