Skip to content

Commit

Permalink
add self-referencing OneToMany associations (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
pscheit committed Nov 12, 2015
1 parent e0d9472 commit d53740b
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 28 deletions.
4 changes: 2 additions & 2 deletions lib/Webforge/Doctrine/Compiler/EntityMappingGenerator.php
Expand Up @@ -122,8 +122,8 @@ protected function generatePropertyAnnotations(GeneratedProperty $property, Gene
protected function generateAssociationAnnotation(GeneratedProperty $property, GeneratedEntity $entity, stdClass $associationPair) {
$annotations = array();

$association = $entity->equals($associationPair->owning->entity) ? $associationPair->owning : $associationPair->inverse;
$hasInverse = isset($associationPair->inverse);
$association = $entity->equals($associationPair->owning->entity) && $property === $associationPair->owning->property ? $associationPair->owning : $associationPair->inverse;
$hasInverse = isset($associationPair->inverse) && !$association->isSelfReferencingUnidirectional();

if ($association->isOneToMany()) {
$annotation = new ORM\OneToMany();
Expand Down
17 changes: 10 additions & 7 deletions lib/Webforge/Doctrine/Compiler/Model.php
Expand Up @@ -73,15 +73,16 @@ public function indexAssociations(GeneratedEntity $entity) {
if ($property->hasReference()) {
$referencedEntity = $property->getReferencedEntity();

if ($referencedEntity->equals($entity)) { // self referencing associations
$isSelfReferencing = $referencedEntity->equals($entity);

/*
$this->createManifestedAssociation(
$entity, $property,
$referencedEntity, $property
);
*/

} elseif (!$this->isAlreadyAssociated($entity, $property)) {

if (!$this->isAlreadyAssociated($entity, $property)) {
// case: we have a relation tag that should find a matching property

if ($property->hasRelationName()) {
Expand All @@ -102,7 +103,7 @@ public function indexAssociations(GeneratedEntity $entity) {
);
}
} else {
// we have no relation tag on us, and we need to find matching candidates in the referencedEntity
// case: we have no relation tag on us, and we need to find matching candidates in the referencedEntity

$unidirectional = TRUE;
foreach ($referencedEntity->getProperties() as $referencedProperty) {
Expand Down Expand Up @@ -186,9 +187,11 @@ protected function isAlreadyAssociated(GeneratedEntity $entity, GeneratedPropert
}

public function completeAssociations() {
//echo "new case\n";
//$this->debugAssociations();
//$this->debugPossibleAssociations();
/*
echo "new case\n";
$this->debugAssociations();
$this->debugPossibleAssociations();
*/

$grouped = array();
$index = array();
Expand Down
55 changes: 43 additions & 12 deletions lib/Webforge/Doctrine/Compiler/ModelAssociation.php
Expand Up @@ -32,24 +32,44 @@ public function __construct(GeneratedEntity $entity, GeneratedProperty $property
$this->referencedEntity = $referencedEntity;
$this->owning = FALSE;

if ($isSelfReferencing = $entity->equals($referencedEntity)) {
$this->owning = TRUE;
$isSelfReferencing = $entity->equals($referencedEntity);

if ($isSelfReferencing) {
// $referencedProperty is always defined, because we will at least find our self as "other" side
$this->referencedProperty = $referencedProperty;

if ($property->isEntityCollection()) {
// @TODO unhandled case: OneToMany self-referencing!!
if ($this->referencedProperty->getName() === $this->property->getName()) {
// the property and referencedProperty are the same
$this->owning = TRUE;

// search for the Many Side of a OneToMany self-referencing association
/*
foreach ($entity->getProperties() as $referencedProperty) {
if ($referencedProperty->hasReference() && $entity->equals($referencedProperty->getReferencedEntity())) {
if ($property->isEntityCollection()) {
$this->type = 'ManyToMany';
} else {
$this->type = 'OneToOne';
}
*/
$this->type = 'ManyToMany';
} else {
$this->type = 'OneToOne';

// the property is another property in this entity
if ($property->isEntityCollection()) {
if ($referencedProperty->isEntityCollection()) {
$this->type = 'ManyToMany';
$this->owning = isset($property->getDefinition()->isOwning) && $property->getDefinition()->isOwning; // this might produce a conflict that no side isOwning (check chis later)
} else {
$this->type = 'OneToMany';
$this->owning = FALSE;
}
} elseif ($property->isEntity()) {
if ($referencedProperty->isEntityCollection()) {
$this->type = 'ManyToOne';
$this->owning = TRUE;
} else {
$this->type = 'OneToOne';
$this->owning = isset($property->getDefinition()->isOwning) && $property->getDefinition()->isOwning;
}
}
}

} elseif ($unidirectional = !isset($referencedProperty)) {
} elseif ($isUnidirectional = !isset($referencedProperty)) {
$this->owning = TRUE;

if ($property->isEntityCollection()) {
Expand Down Expand Up @@ -186,6 +206,17 @@ public function isSelfReferencing() {
return $this->entity->equals($this->referencedEntity);
}


/**
* note: this is not the same as isSelfReferecing() && isUnidrectional()
*
* Return true if the relation is self-referencing and uses the same property for owning and inverse side
* @return boolean
*/
public function isSelfReferencingUnidirectional() {
return $this->isSelfReferencing() && isset($this->referencedProperty) && $this->referencedProperty->getName() === $this->property->getName();
}

public function isEqual(ModelAssociation $other) {
return $this->getUniqueSlug() === $other->getUniqueSlug();
}
Expand Down
18 changes: 16 additions & 2 deletions lib/Webforge/Doctrine/Compiler/Test/ModelBase.php
Expand Up @@ -18,12 +18,26 @@ protected function getVirtualDirectory($name = NULL) {
return $this->getPackageDir('build/package/');
}

protected function assertAssociationMapping($name, \Doctrine\ORM\Mapping\ClassMetadata $metadata) {
protected function assertAssociationMapping($name, \Doctrine\ORM\Mapping\ClassMetadata $metadata, $type = NULL) {
$associations = $metadata->getAssociationMappings();
$this->assertNotEmpty($associations, 'There should be associations defined for entity '.$metadata->name);

$this->assertArrayHasKey($name, $associations, 'association metadata for '.$name.' is not defined in '.$metadata->name);
return $associations[$name];

$association = $associations[$name];

if ($type) {
$const = array(
\Doctrine\ORM\Mapping\ClassMetadata::MANY_TO_ONE=>'ManyToOne',
\Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_MANY=>'OneToMany',
\Doctrine\ORM\Mapping\ClassMetadata::MANY_TO_MANY=>'ManyToMany',
\Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_ONE=>'OneToOne'
);

$this->assertEquals($type, $const[$association['type']], 'Type of Association does not match');
}

return $association;
}

protected function assertTableName($expectedTableName, $entityShortName) {
Expand Down
Expand Up @@ -27,7 +27,7 @@ public function testHasAllSettersGettersAddersRemoversAndCheckers() {
;
}

public function testManyToManyDoctrineMetadata_selfReferencing() {
public function testManyToManyDoctrineMetadata_selfReferencing_unidirectional() {
$metadata = $this->assertDoctrineMetadata($this->categoryClass->getFQN());

$relatedCategories = $this->assertAssociationMapping('relatedCategories', $metadata);
Expand All @@ -38,13 +38,13 @@ public function testManyToManyDoctrineMetadata_selfReferencing() {
$this->assertJoinTable($relatedCategories, 'categories2categories', 'relatedCategories');
}

public function testManyToManyDoctrineMetadata_selfReferencing_withJoinTableName() {
public function testManyToManyDoctrineMetadata_selfReferencing_unidirectional_withJoinTableName() {
$metadata = $this->assertDoctrineMetadata($this->categoryClass->getFQN());

$parentCategories = $this->assertAssociationMapping('parentCategories', $metadata);

$this->assertHasTargetEntity($this->categoryClass, $parentCategories);
$this->assertEmpty($parentCategories['inversedBy']);
$this->assertEmpty($parentCategories['inversedBy'], 'inversedBy should not be set');

$this->assertJoinTable($parentCategories, 'parent_categories', 'parentCategories');
}
Expand Down
51 changes: 51 additions & 0 deletions model-tests/ModelAssociationsOneToManySelfReferencingTest.php
@@ -0,0 +1,51 @@
<?php

namespace Webforge\Doctrine\Compiler;

use ACME\Blog\Entities\Category;
use ACME\Blog\Entities\Tag;
use ACME\Blog\Entities\Author;

class ModelAssociationsOneToManySelfReferencingTest extends \Webforge\Doctrine\Compiler\Test\ModelBase {

public function setUp() {
parent::setUp();

$this->nodeClass = $this->elevateFull('ACME\Blog\Entities\NavigationNode');
}

public function testHasAllSettersGettersAddersRemoversAndCheckers() {
$this->assertThatGClass($this->nodeClass->getParent())
->hasProperty('children')
->isProtected()
->hasMethod('getChildren')
->hasMethod('setChildren')
->hasMethod('addChild')
->hasMethod('removeChild')
->hasMethod('hasChild')

->hasProperty('parent')
->isProtected()
->hasMethod('getParent')
->hasMethod('setParent')
;
}

public function testOneToManyDoctrineMetadata_selfReferencing() {
$metadata = $this->assertDoctrineMetadata($this->nodeClass->getFQN());

// inverse side
$children = $this->assertAssociationMapping('children', $metadata, 'OneToMany');

$this->assertHasTargetEntity($this->nodeClass, $children);
$this->assertEmpty($children['inversedBy'], 'inversedBy should be empty for inverse side '.print_r($children, true));
$this->assertIsMappedBy('parent', $children);

// owning side
$parent = $this->assertAssociationMapping('parent', $metadata, 'ManyToOne');

$this->assertHasTargetEntity($this->nodeClass, $parent);
$this->assertEmpty($parent['mappedBy'], 'mappedBy should be empty for owning side');
$this->assertIsInversedBy('children', $parent);
}
}
2 changes: 2 additions & 0 deletions model-tests/TestReflection.php
Expand Up @@ -13,6 +13,7 @@ public static function entityNames() {
array('ACME\Blog\Entities\Tag', 'Tag'),

array('ACME\Blog\Entities\Page', 'Page'),
array('ACME\Blog\Entities\NavigationNode', 'NavigationNode'),

array('ACME\Blog\Entities\ContentStream\Entry', 'ContentStream/Entry'),
array('ACME\Blog\Entities\ContentStream\TextBlock', 'ContentStream/TextBlock'),
Expand All @@ -29,6 +30,7 @@ public static function entitySlugs() {
array('ACME\Blog\Entities\Category', 'category', 'categories'),
array('ACME\Blog\Entities\Tag', 'tag', 'tags'),
array('ACME\Blog\Entities\Page', 'page', 'pages'),
array('ACME\Blog\Entities\NavigationNode', 'navigation-node', 'navigation-nodes'),

array('ACME\Blog\Entities\ContentStream\Entry', 'content-stream_entry', 'content-stream_entries'),
array('ACME\Blog\Entities\ContentStream\Paragraph', 'content-stream_paragraph', 'content-stream_paragraphs'),
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Expand Up @@ -26,6 +26,7 @@
<file>model-tests/ModelAssociationsManyToManyTest.php</file>
<file>model-tests/ModelAssociationsManyToManyUnidirectionalTest.php</file>
<file>model-tests/ModelAssociationsManyToManySelfReferencingTest.php</file>
<file>model-tests/ModelAssociationsOneToManySelfReferencingTest.php</file>
<file>model-tests/ModelAssociationsOrderByTest.php</file>
<file>model-tests/ModelInhertianceTest.php</file>
<file>model-tests/ModelDCValidationTest.php</file>
Expand Down
2 changes: 1 addition & 1 deletion tests/Webforge/Doctrine/Compiler/EntityGeneratorTest.php
Expand Up @@ -66,7 +66,7 @@ public function testDuplicateTableNameForSelfReferencingAssociations() {
"id": "DefaultId",
"relatedCategories": { "type": "Collection<Category>", "isOwning": true },
"parentCategories": { "type": "Collection<Category>", "isOwning": true }
"parentCategories": { "type": "Collection<Category>", "isOwning": true, "relation": "parentCategories" }
}
}
]
Expand Down
15 changes: 14 additions & 1 deletion tests/files/acme-blog/etc/doctrine/model.json
Expand Up @@ -57,7 +57,7 @@
"posts": { "type": "Collection<Post>" },
"position": { "type": "Integer", "nullable": true },

"relatedCategories": { "type": "Collection<Category>" },
"relatedCategories": { "type": "Collection<Category>", "relation": "relatedCategories" },
"parentCategories": { "type": "Collection<Category>", "joinTableName": "parent_categories" }
},

Expand Down Expand Up @@ -91,6 +91,19 @@
"constructor": ["slug"]
},

{
"name": "NavigationNode",

"properties": {
"id": { "type": "DefaultId" },

"title": { "type":"String", "i18n": true },

"children": { "type": "Collection<NavigationNode>" },
"parent": { "type": "NavigationNode", "nullable": true, "relation": "children" }
}
},

{
"name": "ContentStream\\Stream",

Expand Down

0 comments on commit d53740b

Please sign in to comment.