From 24ce54126d61079198bee7b30406b731f88df4f5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 5 Mar 2026 14:50:14 +0400 Subject: [PATCH 1/4] Fix handling of empty morph types in BelongsToMorphed relation --- src/Relation/BelongsTo.php | 4 ++++ src/Relation/Morphed/BelongsToMorphed.php | 8 +++---- tests/ORM/Fixtures/Image.php | 1 + .../Morphed/BelongsToMorphedRelationTest.php | 21 +++++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Relation/BelongsTo.php b/src/Relation/BelongsTo.php index fdad6bf35..293b122f1 100644 --- a/src/Relation/BelongsTo.php +++ b/src/Relation/BelongsTo.php @@ -91,6 +91,10 @@ public function queue(Pool $pool, Tuple $tuple): void if ($related instanceof ReferenceInterface && $related->hasValue()) { $related = $related->getValue(); $state->setRelation($relName, $related); + if ($related === null) { + $state->setRelationStatus($this->getName(), RelationInterface::STATUS_RESOLVED); + return; + } } if ($related === null) { $this->setNullFromRelated($tuple, false); diff --git a/src/Relation/Morphed/BelongsToMorphed.php b/src/Relation/Morphed/BelongsToMorphed.php index 96cf6aeb7..580f37044 100644 --- a/src/Relation/Morphed/BelongsToMorphed.php +++ b/src/Relation/Morphed/BelongsToMorphed.php @@ -32,10 +32,8 @@ public function initReference(Node $node): ReferenceInterface { $scope = $this->getReferenceScope($node); $nodeData = $node->getData(); - if ($scope === null || !isset($nodeData[$this->morphKey])) { - $result = new Reference($node->getRole(), []); - $result->setValue(null); - return $result; + if (!isset($nodeData[$this->morphKey], $scope)) { + return new EmptyReference('?', null); } // $scope[$this->morphKey] = $nodeData[$this->morphKey]; $target = $nodeData[$this->morphKey]; @@ -48,7 +46,7 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t parent::prepare($pool, $tuple, $related, $load); $related = $tuple->state->getRelation($this->getName()); - if ($related === null) { + if ($related === null || $related instanceof EmptyReference) { return; } diff --git a/tests/ORM/Fixtures/Image.php b/tests/ORM/Fixtures/Image.php index fc168bca8..93b844ecb 100644 --- a/tests/ORM/Fixtures/Image.php +++ b/tests/ORM/Fixtures/Image.php @@ -8,6 +8,7 @@ class Image { public $id; + public $parentType; public $parent; public $url; } diff --git a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php index c0c731f50..0d104b1c5 100644 --- a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php +++ b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php @@ -6,6 +6,7 @@ use Cycle\ORM\Heap\Heap; use Cycle\ORM\Mapper\Mapper; +use Cycle\ORM\Options; use Cycle\ORM\Reference\ReferenceInterface; use Cycle\ORM\Relation; use Cycle\ORM\Schema; @@ -318,6 +319,26 @@ public function testCreateWithoutParentShouldNotSetMorphType(): void $this->assertNull($row[0]['parent_type'], 'parent_type should be NULL for entity without parent'); } + public function testCreateWithoutRelatedButSetMorphTypeManually(): void + { + $schemaArray = $this->getNullableMorphedSchemaArray(); + $this->orm = $this->orm->with( + schema: new Schema($schemaArray), + options: (new Options())->withIgnoreUninitializedRelations(true), + ); + + /** @var Image $c */ + $c = $this->orm->make(Image::class); + $c->url = 'no-parent.png'; + $c->parentType = 'user'; + + $this->save($c); + + $row = $this->getDatabase()->table('image')->select()->where('id', $c->id)->fetchAll(); + $this->assertNull($row[0]['parent_id'], 'parent_id should be NULL for entity without parent'); + $this->assertSame('user', $row[0]['parent_type'], 'parent_type should be NULL for entity without parent'); + } + public function testUpdateRelation(): void { $this->captureReadQueries(); From 7378be99e7b4476b8224f5ce7c800927bfe1eac8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 5 Mar 2026 15:10:35 +0400 Subject: [PATCH 2/4] Don't unset morph_type if the relation was not changed to empty --- src/Relation/Morphed/BelongsToMorphed.php | 7 ++++-- .../Morphed/BelongsToMorphedRelationTest.php | 22 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Relation/Morphed/BelongsToMorphed.php b/src/Relation/Morphed/BelongsToMorphed.php index 580f37044..4f796a244 100644 --- a/src/Relation/Morphed/BelongsToMorphed.php +++ b/src/Relation/Morphed/BelongsToMorphed.php @@ -68,8 +68,11 @@ protected function assertValid(Node $related): void protected function setNullFromRelated(Tuple $tuple, bool $isPreparing): void { - // Set morph key to null - $tuple->state->register($this->morphKey, null); + if ($tuple->node->getRelation($this->getName()) !== null) { + // Set morph key to null if the relation was changed to null + $tuple->state->register($this->morphKey, null); + } + parent::setNullFromRelated($tuple, $isPreparing); } } diff --git a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php index 0d104b1c5..0c8960f11 100644 --- a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php +++ b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php @@ -290,6 +290,7 @@ public function testSetNullShouldClearMorphType(): void $row = $this->getDatabase()->table('image')->select()->where('id', 1)->fetchAll(); $this->assertSame('user', $row[0]['parent_type']); $this->assertNotNull($row[0]['parent_id']); + $this->save($c); $c->parent = null; $this->save($c); @@ -319,7 +320,7 @@ public function testCreateWithoutParentShouldNotSetMorphType(): void $this->assertNull($row[0]['parent_type'], 'parent_type should be NULL for entity without parent'); } - public function testCreateWithoutRelatedButSetMorphTypeManually(): void + public function testUsingFactoryCreateWithoutRelatedButSetMorphTypeManually(): void { $schemaArray = $this->getNullableMorphedSchemaArray(); $this->orm = $this->orm->with( @@ -339,6 +340,25 @@ public function testCreateWithoutRelatedButSetMorphTypeManually(): void $this->assertSame('user', $row[0]['parent_type'], 'parent_type should be NULL for entity without parent'); } + public function testCreateWithoutRelatedButSetMorphTypeManually(): void + { + $schemaArray = $this->getNullableMorphedSchemaArray(); + $this->orm = $this->orm->with( + schema: new Schema($schemaArray), + options: (new Options())->withIgnoreUninitializedRelations(true), + ); + + $c = new Image(); + $c->url = 'no-parent.png'; + $c->parentType = 'user'; + + $this->save($c); + + $row = $this->getDatabase()->table('image')->select()->where('id', $c->id)->fetchAll(); + $this->assertNull($row[0]['parent_id'], 'parent_id should be NULL for entity without parent'); + $this->assertSame('user', $row[0]['parent_type'], 'parent_type should be NULL for entity without parent'); + } + public function testUpdateRelation(): void { $this->captureReadQueries(); From 72455591a4c923c30c8a17852a5e4d862bdddc4e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 5 Mar 2026 15:11:57 +0400 Subject: [PATCH 3/4] Cleanup --- .../Common/Relation/Morphed/BelongsToMorphedRelationTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php index 0c8960f11..de5f26f22 100644 --- a/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php +++ b/tests/ORM/Functional/Driver/Common/Relation/Morphed/BelongsToMorphedRelationTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Heap\Heap; use Cycle\ORM\Mapper\Mapper; -use Cycle\ORM\Options; use Cycle\ORM\Reference\ReferenceInterface; use Cycle\ORM\Relation; use Cycle\ORM\Schema; @@ -325,7 +324,6 @@ public function testUsingFactoryCreateWithoutRelatedButSetMorphTypeManually(): v $schemaArray = $this->getNullableMorphedSchemaArray(); $this->orm = $this->orm->with( schema: new Schema($schemaArray), - options: (new Options())->withIgnoreUninitializedRelations(true), ); /** @var Image $c */ @@ -345,7 +343,6 @@ public function testCreateWithoutRelatedButSetMorphTypeManually(): void $schemaArray = $this->getNullableMorphedSchemaArray(); $this->orm = $this->orm->with( schema: new Schema($schemaArray), - options: (new Options())->withIgnoreUninitializedRelations(true), ); $c = new Image(); From dedb5a62411b99550df0f68c5e1eb54411a7d395 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 5 Mar 2026 15:37:37 +0400 Subject: [PATCH 4/4] Update documentation for BelongsToLoadOptions to clarify applicability for both Belongs To and Refers To relations --- src/Select.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Select.php b/src/Select.php index 4aaabb15d..86d506201 100644 --- a/src/Select.php +++ b/src/Select.php @@ -210,7 +210,7 @@ public function offset(int $offset): self * Available DTO classes (one per relation type): * - {@see \Cycle\ORM\Select\Options\HasOneLoadOptions} * - {@see \Cycle\ORM\Select\Options\HasManyLoadOptions} - * - {@see \Cycle\ORM\Select\Options\BelongsToLoadOptions} + * - {@see \Cycle\ORM\Select\Options\BelongsToLoadOptions} applicable for both Belongs To and Refers To relations * - {@see \Cycle\ORM\Select\Options\ManyToManyLoadOptions} * - {@see \Cycle\ORM\Select\Options\MorphedHasOneLoadOptions} * - {@see \Cycle\ORM\Select\Options\MorphedHasManyLoadOptions}