Skip to content

Commit

Permalink
[bug] discover relations with inheritance (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
NorthBlue333 authored and kbond committed Sep 28, 2022
1 parent ae6bda2 commit 8d41ca8
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 37 deletions.
140 changes: 103 additions & 37 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
class Factory
{
private const NULL_VALUE = '__null_value';

/** @var Configuration|null */
private static $configuration;

Expand Down Expand Up @@ -96,7 +98,11 @@ final public function create($attributes = []): Proxy
// filter each attribute to convert proxies and factories to objects
$mappedAttributes = [];
foreach ($attributes as $name => $value) {
$mappedAttributes[$name] = $this->normalizeAttribute($value, $name);
$normalizedAttribute = $this->normalizeAttribute($value, $name);
if (self::NULL_VALUE === $normalizedAttribute) {
$normalizedAttribute = null;
}
$mappedAttributes[$name] = $normalizedAttribute;
}
$attributes = $mappedAttributes;

Expand Down Expand Up @@ -321,11 +327,16 @@ private function normalizeAttribute($value, ?string $name = null)

if (\is_array($value)) {
// possible OneToMany/ManyToMany relationship
return \array_map(
return \array_filter(
\array_map(
function($value) use ($name) {
return $this->normalizeAttribute($value, $name);
},
$value
),
function($value) {
return $this->normalizeAttribute($value);
},
$value
return self::NULL_VALUE !== $value;
}
);
}

Expand All @@ -340,8 +351,27 @@ function($value) {

// Check if the attribute is cascade persist
if (self::configuration()->hasManagerRegistry()) {
$relationField = $this->relationshipField($name, $value);
$value->cascadePersist = $this->hasCascadePersist($value, $relationField);
$ownedRelationshipField = $this->ownedRelationshipField($name, $value);

if (null !== $ownedRelationshipField) {
$cascadePersist = $this->hasCascadePersist(null, $ownedRelationshipField);
} else {
$isCollection = false;
$relationshipField = $this->inverseRelationshipField($name, $value, $isCollection);
$cascadePersist = $this->hasCascadePersist($value, $relationshipField);

if ($this->isPersisting() && null !== $relationshipField && false === $cascadePersist) {
$this->afterPersist[] = static function(Proxy $proxy) use ($value, $relationshipField, $isCollection) {
$value->create([$relationshipField => $isCollection ? [$proxy] : $proxy]);
$proxy->refresh();
};

// creation delegated to afterPersist event - return null here
return self::NULL_VALUE;
}
}

$value->cascadePersist = $cascadePersist;
}

return $value->create()->object();
Expand All @@ -356,10 +386,10 @@ private static function normalizeObject(object $object): object
}
}

private function normalizeCollection(?string $fieldName, FactoryCollection $collection): array
private function normalizeCollection(?string $relationName, FactoryCollection $collection): array
{
if ($this->isPersisting()) {
$field = $this->inverseRelationshipField($fieldName, $collection->factory());
$field = $this->inverseCollectionRelationshipField($relationName, $collection->factory());
$cascadePersist = $this->hasCascadePersist($collection->factory(), $field);

if ($field && false === $cascadePersist) {
Expand All @@ -383,7 +413,7 @@ function(self $factory) {
);
}

private function relationshipField(?string $relationName, self $factory): ?string
private function ownedRelationshipField(?string $relationName, self $factory): ?string
{
$factoryClass = $this->class;
$relationClass = $factory->class;
Expand All @@ -395,83 +425,119 @@ private function relationshipField(?string $relationName, self $factory): ?strin
return null;
}

try {
// Check mappedBy side ($factory is the owner of the relation)
$relationClassMetadata = self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass);
} catch (\RuntimeException $e) {
// relation not managed - could be embeddable
$relationClassMetadata = null;
}

if (null !== $relationName && $factoryClassMetadata->hasAssociation($relationName) && \is_a($factory->class, $factoryClassMetadata->getAssociationTargetclass($relationName), true)) {
if ($factoryClassMetadata->isAssociationInverseSide($relationName)) {
$mappedBy = $factoryClassMetadata->getAssociationMappedByTargetField($relationName);
if (null !== $relationClassMetadata && $relationClassMetadata->hasAssociation($mappedBy) && ($relationClassMetadata->isSingleValuedAssociation($mappedBy) || $relationClassMetadata->isCollectionValuedAssociation($mappedBy)) && \is_a($factoryClass, $relationClassMetadata->getAssociationTargetClass($mappedBy), true)) {
return $mappedBy;
}
} else {
if (null !== $relationName && $factoryClassMetadata->hasAssociation($relationName)) {
if (\is_a($factory->class, $factoryClassMetadata->getAssociationTargetclass($relationName), true) && !$factoryClassMetadata->isAssociationInverseSide($relationName)) {
return $relationName;
}
} else {
$relationName = null;
}

foreach ($factoryClassMetadata->getAssociationNames() as $field) {
if (!$factoryClassMetadata->isAssociationInverseSide($field) && $factoryClassMetadata->getAssociationTargetClass($field) === $relationClass) {
if (
!$factoryClassMetadata->isAssociationInverseSide($field)
&& \is_a($relationClass, $factoryClassMetadata->getAssociationTargetClass($field), true)
&& (null === $relationName || ($factoryClassMetadata->getAssociationMapping($field)['inversedBy'] ?? null) === $relationName)
) {
return $field;
}
}

if (null === $relationClassMetadata) {
return null; // no relationship found
}

private function inverseRelationshipField(?string $relationName, self $factory, ?bool &$isCollectionValuedRelation): ?string
{
$factoryClass = $this->class;
$relationClass = $factory->class;

// Check inversedBy side ($this is the owner of the relation)
$factoryClassMetadata = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);

if (!$factoryClassMetadata instanceof ORMClassMetadata) {
return null;
}

if ($factoryClassMetadata->hasAssociation($relationName)) {
$relationName = $factoryClassMetadata->getAssociationMappedByTargetField($relationName) ?? null;
} else {
$relationName = null;
}

try {
// Check mappedBy side ($factory is the owner of the relation)
$relationClassMetadata = self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass);
} catch (\RuntimeException $e) {
// relation not managed - could be embeddable
return null;
}

$isCollectionValuedRelation = false;
foreach ($relationClassMetadata->getAssociationNames() as $field) {
if (($relationClassMetadata->isSingleValuedAssociation($field) || $relationClassMetadata->isCollectionValuedAssociation($field)) && $relationClassMetadata->getAssociationTargetClass($field) === $factoryClass) {
if (
($relationClassMetadata->isSingleValuedAssociation($field) || $relationClassMetadata->isCollectionValuedAssociation($field))
&& \is_a($factoryClass, $relationClassMetadata->getAssociationTargetClass($field), true)
&& (null === $relationName || !$relationClassMetadata instanceof ORMClassMetadata || $field === $relationName)
) {
$isCollectionValuedRelation = $relationClassMetadata->isCollectionValuedAssociation($field);

return $field;
}
}

return null; // no relationship found
}

private function inverseRelationshipField(?string $relationName, self $factory): ?string
private function inverseCollectionRelationshipField(?string $relationName, self $factory): ?string
{
$factoryClass = $this->class;
$collectionClass = $factory->class;
$factoryClassMetadata = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);
$collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);

if (null !== $relationName && $factoryClassMetadata instanceof ORMClassMetadata && $factoryClassMetadata->hasAssociation($relationName) && \is_a($factory->class, $factoryClassMetadata->getAssociationTargetclass($relationName), true) && null !== $mappedBy = $factoryClassMetadata->getAssociationMappedByTargetField($relationName)) {
if ($collectionMetadata->isSingleValuedAssociation($mappedBy)) {
if (null !== $relationName && $factoryClassMetadata instanceof ORMClassMetadata && $factoryClassMetadata->hasAssociation($relationName)) {
$mappedBy = $factoryClassMetadata->getAssociationMappedByTargetField($relationName);
if (
\is_a($factory->class, $factoryClassMetadata->getAssociationTargetclass($relationName), true)
&& null !== $mappedBy
&& $collectionMetadata->isSingleValuedAssociation($mappedBy)
) {
return $mappedBy;
}
} else {
$relationName = null;
}

foreach ($collectionMetadata->getAssociationNames() as $field) {
// ensure 1-n and associated class matches
if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
if (
$collectionMetadata->isSingleValuedAssociation($field)
&& \is_a($this->class, $collectionMetadata->getAssociationTargetClass($field), true)
&& (!$collectionMetadata instanceof ORMClassMetadata || null === $relationName || ($collectionMetadata->getAssociationMapping($field)['inversedBy'] ?? null) === $relationName)
) {
return $field;
}
}

return null; // no relationship found
}

private function hasCascadePersist(self $factory, ?string $field): bool
private function hasCascadePersist(?self $factory, ?string $field): bool
{
if (null === $field) {
return false;
}

$factoryClass = $this->class;
$relationClass = $factory->class;
$relationClass = $factory->class ?? null;
$classMetadataFactory = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);
$relationClassMetadata = self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass);
$relationClassMetadata = null !== $relationClass ? self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass) : null;

if (!$relationClassMetadata instanceof ClassMetadataInfo || !$classMetadataFactory instanceof ClassMetadataInfo) {
if (!$classMetadataFactory instanceof ClassMetadataInfo) {
return false;
}

if ($relationClassMetadata->hasAssociation($field)) {
if ($relationClassMetadata instanceof ClassMetadataInfo && $relationClassMetadata->hasAssociation($field)) {
$inversedBy = $relationClassMetadata->getAssociationMapping($field)['inversedBy'];
if (null === $inversedBy) {
return false;
Expand Down
7 changes: 7 additions & 0 deletions tests/Fixtures/Entity/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ public function getUser(): User
return $this->user;
}

public function setUser(?User $user): self
{
$this->user = $user;

return $this;
}

public function getBody(): ?string
{
return $this->body;
Expand Down
55 changes: 55 additions & 0 deletions tests/Fixtures/Entity/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
/**
* @ORM\Entity(repositoryClass="Zenstruck\Foundry\Tests\Fixtures\Repository\PostRepository")
* @ORM\Table(name="posts")
* @ORM\InheritanceType(value="SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type")
* @ORM\DiscriminatorMap({"simple": Post::class, "specific": SpecificPost::class})
*/
class Post
{
Expand Down Expand Up @@ -96,6 +99,16 @@ class Post
*/
private $lessRelevantRelatedToPost;

/**
* @ORM\ManyToMany(targetEntity=Post::class, inversedBy="relatedToPosts")
*/
private $relatedPosts;

/**
* @ORM\ManyToMany(targetEntity=Post::class, mappedBy="relatedPosts")
*/
private $relatedToPosts;

public function __construct(string $title, string $body, ?string $shortDescription = null)
{
$this->title = $title;
Expand All @@ -105,6 +118,8 @@ public function __construct(string $title, string $body, ?string $shortDescripti
$this->tags = new ArrayCollection();
$this->secondaryTags = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->relatedPosts = new ArrayCollection();
$this->relatedToPosts = new ArrayCollection();
}

public function __toString(): string
Expand Down Expand Up @@ -187,6 +202,46 @@ public function setPublishedAt(\DateTime $timestamp)
$this->publishedAt = $timestamp;
}

public function getRelatedPosts()
{
return $this->relatedPosts;
}

public function addRelatedPost(self $relatedPost)
{
if (!$this->relatedPosts->contains($relatedPost)) {
$this->relatedPosts[] = $relatedPost;
}
}

public function removeRelatedPost(self $relatedPost)
{
if ($this->relatedPosts->contains($relatedPost)) {
$this->relatedPosts->removeElement($relatedPost);
}
}

public function getRelatedToPosts()
{
return $this->relatedToPosts;
}

public function addRelatedToPost(self $relatedToPost)
{
if (!$this->relatedToPosts->contains($relatedToPost)) {
$this->relatedToPosts[] = $relatedToPost;
$relatedToPost->addRelatedPost($this);
}
}

public function removeRelatedToPost(self $relatedToPost)
{
if ($this->relatedToPosts->contains($relatedToPost)) {
$this->relatedToPosts->removeElement($relatedToPost);
$relatedToPost->removeRelatedPost($this);
}
}

public function getTags()
{
return $this->tags;
Expand Down
28 changes: 28 additions & 0 deletions tests/Fixtures/Entity/SpecificPost.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Zenstruck\Foundry\Tests\Fixtures\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class SpecificPost extends Post
{
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $specificProperty;

public function getSpecificProperty()
{
return $this->specificProperty;
}

public function setSpecificProperty($specificProperty)
{
$this->specificProperty = $specificProperty;

return $this;
}
}
Loading

0 comments on commit 8d41ca8

Please sign in to comment.