Skip to content

Commit

Permalink
fix: detect if relation is oneToOne (#732)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil authored Dec 10, 2024
1 parent 59867c3 commit af64c35
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 11 deletions.
16 changes: 12 additions & 4 deletions src/ORM/OrmV2PersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel
{
$metadata = $this->classMetadata($parent);

$association = $this->getAssociationMapping($parent, $field);
$association = $this->getAssociationMapping($parent, $child, $field);

if ($association) {
return new RelationshipMetadata(
isCascadePersist: $association['isCascadePersist'],
inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null,
isCollection: $metadata->isCollectionValuedAssociation($association['fieldName']),
isOneToOne: $association['type'] === ClassMetadataInfo::ONE_TO_ONE
);
}

$inversedAssociation = $this->getAssociationMapping($child, $field);
$inversedAssociation = $this->getAssociationMapping($child, $parent, $field);

if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) {
return null;
Expand Down Expand Up @@ -70,6 +71,7 @@ public function relationshipMetadata(string $parent, string $child, string $fiel
isCascadePersist: $inversedAssociation['isCascadePersist'],
inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null,
isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']),
isOneToOne: $inversedAssociation['type'] === ClassMetadataInfo::ONE_TO_ONE
);
}

Expand All @@ -78,12 +80,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel
* @return array[]|null
* @phpstan-return AssociationMapping|null
*/
private function getAssociationMapping(string $entityClass, string $field): ?array
private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?array
{
try {
return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
$associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
} catch (MappingException|ORMMappingException) {
return null;
}

if (!is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) {
return null;
}

return $associationMapping;
}
}
21 changes: 15 additions & 6 deletions src/ORM/OrmV3PersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\InverseSideMapping;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Mapping\OneToOneAssociationMapping;
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
use Doctrine\Persistence\Mapping\MappingException;
use Zenstruck\Foundry\Persistence\RelationshipMetadata;
Expand All @@ -27,17 +28,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel
{
$metadata = $this->classMetadata($parent);

$association = $this->getAssociationMapping($parent, $field);
$association = $this->getAssociationMapping($parent, $child, $field);

if ($association) {
return new RelationshipMetadata(
isCascadePersist: $association->isCascadePersist(),
inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null,
isCollection: $association instanceof ToManyAssociationMapping
isCollection: $association instanceof ToManyAssociationMapping,
isOneToOne: $association instanceof OneToOneAssociationMapping,
);
}

$inversedAssociation = $this->getAssociationMapping($child, $field);
$inversedAssociation = $this->getAssociationMapping($child, $parent, $field);

if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) {
return null;
Expand All @@ -60,19 +62,26 @@ public function relationshipMetadata(string $parent, string $child, string $fiel
return new RelationshipMetadata(
isCascadePersist: $inversedAssociation->isCascadePersist(),
inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null,
isCollection: $inversedAssociation instanceof ToManyAssociationMapping
isCollection: $inversedAssociation instanceof ToManyAssociationMapping,
isOneToOne: $inversedAssociation instanceof OneToOneAssociationMapping,
);
}

/**
* @param class-string $entityClass
*/
private function getAssociationMapping(string $entityClass, string $field): ?AssociationMapping
private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?AssociationMapping
{
try {
return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
$associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
} catch (MappingException|ORMMappingException) {
return null;
}

if (!is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) {
return null;
}

return $associationMapping;
}
}
5 changes: 4 additions & 1 deletion src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@ protected function normalizeParameter(string $field, mixed $value): mixed
$relationshipMetadata = $pm->relationshipMetadata($value::class(), static::class(), $field);

// handle inversed OneToOne
if ($relationshipMetadata && !$relationshipMetadata->isCollection && $inverseField = $relationshipMetadata->inverseField) {
if ($relationshipMetadata
&& $relationshipMetadata->isOneToOne
&& !$relationshipMetadata->isCollection
&& $inverseField = $relationshipMetadata->inverseField) {
// we create now the object to prevent "non-nullable" property errors,
// but we'll need to remove it once the current object is created
$inversedObject = unproxy($value->create());
Expand Down
1 change: 1 addition & 0 deletions src/Persistence/RelationshipMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(
public readonly bool $isCascadePersist,
public readonly ?string $inverseField,
public readonly bool $isCollection,
public readonly bool $isOneToOne,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_inverse_side')]
class InverseSide
{
#[ORM\Id]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_owning_side')]
class OwningSide
{
#[ORM\Id]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing;

use Doctrine\ORM\Mapping as ORM;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('many_to_one_to_self_referencing_owning_side')]
class OwningSide
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public ?int $id = null;

#[ORM\ManyToOne(inversedBy: 'owningSide')]
public ?SelfReferencingInverseSide $inverseSide = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing;

use Doctrine\ORM\Mapping as ORM;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('many_to_one_to_self_referencing_inverse_side')]
class SelfReferencingInverseSide
{
#[ORM\Id]
#[ORM\Column]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public ?int $id = null;

#[ORM\ManyToOne()]
public ?SelfReferencingInverseSide $inverseSide = null;

#[ORM\OneToOne(mappedBy: 'inverseSide')]
public ?OwningSide $owningSide = null;
}
15 changes: 15 additions & 0 deletions tests/Integration/ORM/EdgeCasesRelationshipTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning;
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing;
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity;
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship;
use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity;
Expand Down Expand Up @@ -123,6 +124,20 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void
self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide);
}

/**
* @test
*/
public function many_to_many_to_self_referencing_inverse_side(): void
{
$owningSideFactory = persistent_factory(ManyToOneToSelfReferencing\OwningSide::class);
$inverseSideFactory = persistent_factory(ManyToOneToSelfReferencing\SelfReferencingInverseSide::class);

$owningSideFactory->create(['inverseSide' => $inverseSideFactory]);

$owningSideFactory::assert()->count(1);
$inverseSideFactory::assert()->count(1);
}

/**
* @test
*/
Expand Down

0 comments on commit af64c35

Please sign in to comment.