Skip to content

Commit

Permalink
fix: can create inversed one to one (#659)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil authored Nov 22, 2024
1 parent 6d08784 commit f48ffd1
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 37 deletions.
46 changes: 33 additions & 13 deletions src/ORM/OrmV2PersistenceStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,47 @@ public function relationshipMetadata(string $parent, string $child, string $fiel

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

if (null === $association) {
$inversedAssociation = $this->getAssociationMapping($child, $field);
if ($association) {
return new RelationshipMetadata(
isCascadePersist: $association['isCascadePersist'],
inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null,
isCollection: $metadata->isCollectionValuedAssociation($association['fieldName']),
);
}

if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) {
return null;
}
$inversedAssociation = $this->getAssociationMapping($child, $field);

if (!\is_a($parent, $inversedAssociation['targetEntity'], allow_string: true)) { // is_a() handles inheritance as well
throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
}
if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) {
return null;
}

if (ClassMetadataInfo::ONE_TO_MANY !== $inversedAssociation['type'] || !isset($inversedAssociation['mappedBy'])) {
return null;
}
if (!\is_a(
$parent,
$inversedAssociation['targetEntity'],
allow_string: true
)) { // is_a() handles inheritance as well
throw new \LogicException(
"Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"
);
}

$association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']);
if (!in_array(
$inversedAssociation['type'],
[ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE],
true
)
|| !isset($inversedAssociation['mappedBy'])
) {
return null;
}

$association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']);
$inversedAssociationMetadata = $this->classMetadata($inversedAssociation['sourceEntity']);

return new RelationshipMetadata(
isCascadePersist: $association['isCascadePersist'],
isCascadePersist: $inversedAssociation['isCascadePersist'],
inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null,
isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']),
);
}

Expand Down
40 changes: 27 additions & 13 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\ToManyAssociationMapping;
use Doctrine\Persistence\Mapping\MappingException;
use Zenstruck\Foundry\Persistence\RelationshipMetadata;

Expand All @@ -28,27 +29,40 @@ public function relationshipMetadata(string $parent, string $child, string $fiel

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

if (null === $association) {
$inversedAssociation = $this->getAssociationMapping($child, $field);
if ($association) {
return new RelationshipMetadata(
isCascadePersist: $association->isCascadePersist(),
inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null,
isCollection: $association instanceof ToManyAssociationMapping
);
}

if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) {
return null;
}
$inversedAssociation = $this->getAssociationMapping($child, $field);

if (!\is_a($parent, $inversedAssociation->targetEntity, allow_string: true)) { // is_a() handles inheritance as well
throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
}
if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) {
return null;
}

if (!$inversedAssociation instanceof InverseSideMapping) {
return null;
}
if (!\is_a(
$parent,
$inversedAssociation->targetEntity,
allow_string: true
)) { // is_a() handles inheritance as well
throw new \LogicException(
"Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"
);
}

$association = $metadata->getAssociationMapping($inversedAssociation->mappedBy);
if (!$inversedAssociation instanceof InverseSideMapping) {
return null;
}

$association = $metadata->getAssociationMapping($inversedAssociation->mappedBy);

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

Expand Down
21 changes: 19 additions & 2 deletions src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,25 @@ protected function normalizeParameter(string $field, mixed $value): mixed
$value->persist = $this->persist; // todo - breaks immutability
}

if ($value instanceof self && Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) {
$value->persist = false;
if ($value instanceof self) {
$pm = Configuration::instance()->persistence();

$relationshipMetadata = $pm->relationshipMetadata($value::class(), static::class(), $field);

// handle inversed OneToOne
if ($relationshipMetadata && !$relationshipMetadata->isCollection && $inverseField = $relationshipMetadata->inverseField) {
$this->tempAfterPersist[] = static function(object $object) use ($value, $inverseField, $pm) {
$value->create([$inverseField => $object]);
$pm->refresh($object);
};

// creation delegated to afterPersist hook - return empty array here
return null;
}

if (Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) {
$value->persist = false;
}
}

return unproxy(parent::normalizeParameter($field, $value));
Expand Down
1 change: 1 addition & 0 deletions src/Persistence/RelationshipMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class RelationshipMetadata
public function __construct(
public readonly bool $isCascadePersist,
public readonly ?string $inverseField,
public readonly bool $isCollection,
) {
}
}
12 changes: 12 additions & 0 deletions tests/Fixture/Entity/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#[ORM\MappedSuperclass]
abstract class Address extends Base
{
protected Contact|null $contact = null;

#[ORM\Column(length: 255)]
private string $city;

Expand All @@ -28,6 +30,16 @@ public function __construct(string $city)
$this->city = $city;
}

public function getContact(): Contact|null
{
return $this->contact;
}

public function setContact(Contact|null $contact): void
{
$this->contact = $contact;
}

public function getCity(): string
{
return $this->city;
Expand Down
4 changes: 4 additions & 0 deletions tests/Fixture/Entity/Address/CascadeAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Entity\Address;
use Zenstruck\Foundry\Tests\Fixture\Entity\Contact;
use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[ORM\Entity]
class CascadeAddress extends Address
{
#[ORM\OneToOne(targetEntity: CascadeContact::class, mappedBy: 'address', cascade: ['persist', 'remove'])]
protected Contact|null $contact = null;
}
4 changes: 4 additions & 0 deletions tests/Fixture/Entity/Address/StandardAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Entity\Address;
use Zenstruck\Foundry\Tests\Fixture\Entity\Contact;
use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[ORM\Entity]
class StandardAddress extends Address
{
#[ORM\OneToOne(targetEntity: StandardContact::class, mappedBy: 'address')]
protected Contact|null $contact = null;
}
2 changes: 1 addition & 1 deletion tests/Fixture/Entity/Contact/CascadeContact.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class CascadeContact extends Contact
#[ORM\JoinTable(name: 'category_tag_cascade_secondary')]
protected Collection $secondaryTags;

#[ORM\OneToOne(targetEntity: CascadeAddress::class, cascade: ['persist', 'remove'])]
#[ORM\OneToOne(targetEntity: CascadeAddress::class, inversedBy: 'contact', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false)]
protected Address $address;
}
24 changes: 16 additions & 8 deletions tests/Integration/ORM/EntityFactoryRelationshipTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,6 @@ public function one_to_one_owning(): void
$this->assertNotNull($contact->getAddress()->id);
}

/**
* @test
*/
public function one_to_one_inverse(): void
{
$this->markTestSkipped('Not supported. Should it be?');
}

/**
* @test
*/
Expand Down Expand Up @@ -292,6 +284,22 @@ public function inverse_many_to_many_with_two_relationships_same_entity(): void
static::contactFactory()::assert()->count(6);
}

/**
* @test
*/
public function inversed_one_to_one(): void
{
$addressFactory = $this->addressFactory();
$contactFactory = $this->contactFactory();

$address = $addressFactory->create(['contact' => $contactFactory]);

self::assertNotNull($address->getContact());

$addressFactory::assert()->count(1);
$contactFactory::assert()->count(1);
}

/**
* @test
*/
Expand Down

0 comments on commit f48ffd1

Please sign in to comment.