Skip to content

Commit f95b89c

Browse files
committed
Clarify Collection APIs
- Rename `next` to `connectsTo` and `more` to `hasMore` for clarity - Make `mapper` and `hydrator` properties `private(set)`, add `bindMapper()` - Remove `$changes` from Collection::persist() — identity map merge handles it - Remove EntityFactory::withChanges() and AbstractMapper::replaceTracked() - Register entities in identity map at insert time for pre-flush dedup - Preserve original pending operation in tryMergeWithIdentityMap
1 parent 4055991 commit f95b89c

12 files changed

Lines changed: 107 additions & 289 deletions

src/AbstractMapper.php

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ public function markTracked(object $entity, Collection $collection): bool
8080

8181
public function persist(object $object, Collection $onCollection): object
8282
{
83-
$next = $onCollection->next;
84-
if ($onCollection instanceof Filtered && $next !== null) {
85-
$this->persist($object, $next);
83+
$connectsTo = $onCollection->connectsTo;
84+
if ($onCollection instanceof Filtered && $connectsTo !== null) {
85+
$this->persist($object, $connectsTo);
8686

8787
return $object;
8888
}
@@ -103,6 +103,7 @@ public function persist(object $object, Collection $onCollection): object
103103

104104
$this->pending[$object] = 'insert';
105105
$this->markTracked($object, $onCollection);
106+
$this->registerInIdentityMap($object, $onCollection);
106107

107108
return $object;
108109
}
@@ -123,21 +124,9 @@ public function isTracked(object $entity): bool
123124
return $this->tracked->offsetExists($entity);
124125
}
125126

126-
public function replaceTracked(object $old, object $new, Collection $onCollection): void
127-
{
128-
$op = $this->pending[$old] ?? 'update';
129-
$this->tracked->offsetUnset($old);
130-
$this->pending->offsetUnset($old);
131-
$this->evictFromIdentityMap($old, $onCollection);
132-
133-
$this->markTracked($new, $onCollection);
134-
$this->registerInIdentityMap($new, $onCollection);
135-
$this->pending[$new] = $op;
136-
}
137-
138127
public function registerCollection(string $alias, Collection $collection): void
139128
{
140-
$collection->mapper = $this;
129+
$collection->bindMapper($this);
141130
$this->collections[$alias] = $collection;
142131
}
143132

@@ -199,7 +188,7 @@ protected function evictFromIdentityMap(object $entity, Collection $coll): void
199188

200189
protected function findInIdentityMap(Collection $collection): object|null
201190
{
202-
if ($collection->name === null || !is_scalar($collection->condition) || $collection->more) {
191+
if ($collection->name === null || !is_scalar($collection->condition) || $collection->hasMore) {
203192
return null;
204193
}
205194

@@ -234,6 +223,8 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje
234223
$this->entityFactory->set($entity, $idName, $idValue);
235224
}
236225

226+
$op = $this->pending[$existing] ?? 'update';
227+
237228
if ($this->entityFactory->isReadOnly($existing)) {
238229
$merged = $this->entityFactory->mergeEntities($existing, $entity);
239230

@@ -245,7 +236,7 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje
245236
$this->registerInIdentityMap($merged, $coll);
246237
}
247238

248-
$this->pending[$merged] = 'update';
239+
$this->pending[$merged] = $op;
249240

250241
return $merged;
251242
}
@@ -258,7 +249,7 @@ private function tryMergeWithIdentityMap(object $entity, Collection $coll): obje
258249
$this->markTracked($existing, $coll);
259250
}
260251

261-
$this->pending[$existing] = 'update';
252+
$this->pending[$existing] = $op;
262253

263254
return $existing;
264255
}
@@ -290,9 +281,8 @@ public function __get(string $name): Collection
290281
}
291282

292283
$coll = new Collection($name);
293-
$coll->mapper = $this;
294284

295-
return $coll;
285+
return $coll->bindMapper($this);
296286
}
297287

298288
public function __isset(string $alias): bool
@@ -310,14 +300,10 @@ public function __call(string $name, array $arguments): Collection
310300
{
311301
if (isset($this->collections[$name])) {
312302
$collection = clone $this->collections[$name];
313-
$collection->mapper = $this;
314303

315-
return $collection->with(...$arguments);
304+
return $collection->bindMapper($this)->with(...$arguments);
316305
}
317306

318-
$collection = Collection::__callstatic($name, $arguments);
319-
$collection->mapper = $this;
320-
321-
return $collection;
307+
return Collection::__callstatic($name, $arguments)->bindMapper($this);
322308
}
323309
}

src/CollectionIterator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ public function key(): string
5151

5252
public function hasChildren(): bool
5353
{
54-
return $this->current()->more;
54+
return $this->current()->hasMore;
5555
}
5656

5757
public function getChildren(): RecursiveArrayIterator
5858
{
5959
$c = $this->current();
6060
$pool = $c->hasChildren ? $c->children : [];
61-
if ($c->next !== null) {
62-
$pool[] = $c->next;
61+
if ($c->connectsTo !== null) {
62+
$pool[] = $c->connectsTo;
6363
}
6464

6565
return new static(

src/Collections/Collection.php

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ class Collection implements ArrayAccess
1414
{
1515
public private(set) bool $required = true;
1616

17-
public AbstractMapper|null $mapper = null;
17+
public private(set) AbstractMapper|null $mapper = null;
1818

19-
public Hydrator|null $hydrator = null;
19+
public private(set) Hydrator|null $hydrator = null;
2020

2121
public private(set) Collection|null $parent = null;
2222

23-
public private(set) Collection|null $next = null;
23+
public private(set) Collection|null $connectsTo = null;
2424

2525
private Collection|null $last = null;
2626

@@ -29,7 +29,7 @@ class Collection implements ArrayAccess
2929

3030
public bool $hasChildren { get => !empty($this->children); }
3131

32-
public bool $more { get => $this->hasChildren || $this->next !== null; }
32+
public bool $hasMore { get => $this->hasChildren || $this->connectsTo !== null; }
3333

3434
/** @var array<scalar, mixed>|scalar|null */
3535
public private(set) array|int|float|string|bool|null $condition = [];
@@ -50,22 +50,9 @@ public function addChild(Collection $child): void
5050
$this->children[] = $clone;
5151
}
5252

53-
public function persist(object $object, mixed ...$changes): object
53+
public function persist(object $object): object
5454
{
55-
$mapper = $this->resolveMapper();
56-
57-
if ($changes) {
58-
$original = $object;
59-
$object = $mapper->entityFactory->withChanges($original, ...$changes);
60-
61-
if ($mapper->isTracked($original)) {
62-
$mapper->replaceTracked($original, $object, $this);
63-
64-
return $object;
65-
}
66-
}
67-
68-
return $mapper->persist($object, $this);
55+
return $this->resolveMapper()->persist($object, $this);
6956
}
7057

7158
public function remove(object $object): bool
@@ -106,6 +93,14 @@ public function offsetUnset(mixed $offset): void
10693
// no-op
10794
}
10895

96+
/** @internal Used by AbstractMapper to bind this collection */
97+
public function bindMapper(AbstractMapper $mapper): static
98+
{
99+
$this->mapper = $mapper;
100+
101+
return $this;
102+
}
103+
109104
public function hydrateFrom(Hydrator $hydrator): static
110105
{
111106
$this->hydrator = $hydrator;
@@ -116,7 +111,7 @@ public function hydrateFrom(Hydrator $hydrator): static
116111
public function stack(Collection $collection): static
117112
{
118113
$tail = $this->last ?? $this;
119-
$tail->setNext($collection);
114+
$tail->setConnectsTo($collection);
120115
$this->last = $collection->last ?? $collection;
121116

122117
return $this;
@@ -151,10 +146,10 @@ private function resolveMapper(): AbstractMapper
151146
return $this->findMapper() ?? throw new CollectionNotBound($this->name);
152147
}
153148

154-
private function setNext(Collection $collection): void
149+
private function setConnectsTo(Collection $collection): void
155150
{
156151
$collection->parent = $this;
157-
$this->next = $collection;
152+
$this->connectsTo = $collection;
158153
}
159154

160155
/** @param array<int, mixed> $arguments */
@@ -187,9 +182,9 @@ public function __call(string $name, array $children): static
187182

188183
public function __clone(): void
189184
{
190-
if ($this->next !== null) {
191-
$this->next = clone $this->next;
192-
$this->next->parent = $this;
185+
if ($this->connectsTo !== null) {
186+
$this->connectsTo = clone $this->connectsTo;
187+
$this->connectsTo->parent = $this;
193188
}
194189

195190
$clonedChildren = [];
@@ -209,8 +204,8 @@ public function __clone(): void
209204

210205
$node = $this;
211206

212-
while ($node->next !== null) {
213-
$node = $node->next;
207+
while ($node->connectsTo !== null) {
208+
$node = $node->connectsTo;
214209
}
215210

216211
$this->last = $node !== $this ? $node : null;

src/EntityFactory.php

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
use ReflectionUnionType;
1212

1313
use function array_key_exists;
14-
use function array_keys;
1514
use function class_exists;
16-
use function implode;
1715
use function is_array;
1816
use function is_bool;
1917
use function is_float;
@@ -124,41 +122,6 @@ public function create(string $class, mixed ...$properties): object
124122
return $entity;
125123
}
126124

127-
public function withChanges(object $entity, mixed ...$changes): object
128-
{
129-
$clone = $this->reflectClass($entity::class)->newInstanceWithoutConstructor();
130-
$styledChanges = [];
131-
foreach ($changes as $prop => $value) {
132-
$styledChanges[$this->style->styledProperty((string) $prop)] = $value;
133-
}
134-
135-
foreach ($this->reflectProperties($entity::class) as $name => $prop) {
136-
if (array_key_exists($name, $styledChanges)) {
137-
$value = $styledChanges[$name];
138-
$coerced = $this->coerce($prop, $value);
139-
140-
if ($coerced === null && !($prop->getType()?->allowsNull() ?? false)) {
141-
throw new DomainException(
142-
'Invalid value for ' . $entity::class . '::$' . $name,
143-
);
144-
}
145-
146-
$prop->setValue($clone, $coerced);
147-
unset($styledChanges[$name]);
148-
} elseif ($prop->isInitialized($entity)) {
149-
$prop->setValue($clone, $prop->getValue($entity));
150-
}
151-
}
152-
153-
if ($styledChanges) {
154-
throw new DomainException(
155-
'Unknown properties for ' . $entity::class . ': ' . implode(', ', array_keys($styledChanges)),
156-
);
157-
}
158-
159-
return $clone;
160-
}
161-
162125
public function mergeEntities(object $base, object $overlay): object
163126
{
164127
if ($base::class !== $overlay::class) {

src/Hydrators/Nested.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ private function hydrateNode(
5959

6060
$entities[$entity] = $collection;
6161

62-
if ($collection->next !== null) {
63-
$this->hydrateChild($data, $collection->next, $entityFactory, $entities);
62+
if ($collection->connectsTo !== null) {
63+
$this->hydrateChild($data, $collection->connectsTo, $entityFactory, $entities);
6464
}
6565

6666
foreach ($collection->children as $child) {

0 commit comments

Comments
 (0)