Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[DDC-2042] Added "targetEntity" to AssociationOverride #633

Closed
wants to merge 1 commit into from

7 participants

@spezifanta

Hey,

I needed to override "targetEntity" so I forked Doctrine and applyed it myself. Just after that I found a Ticket [DDC-2042] in your Jira so I added a unit test to and would be more than happy to see this commit get merged in to the main master (at some point ;)).

Cheers, Alex

I will stick around in the #doctrine-dev on Freenode for any questions. My nick there is "fanta".

Keep up the good work.

/**

  • @ORM\Entity
  • @ORM\Table(name="customer")
  • @ORM\AssociationOverrides({
  • @ORM\AssociationOverride(name="products",
  • targetEntity="MyNewProduct"
  • )
  • }) */
@spezifanta spezifanta [DDC-2042] Added "targetEntity" to AssociationOverride
Having the possibility to override "targetEntity" lets the user the
opportunity to change assosciations of sub classes without the need
of changing it's parent class.

Lets say we have the following classes:
- Customer (MappedSuperClass)
-- CustomerDE (Entity)
-- CustomerEN (Entity)
- Product (MappedSuperClass)
-- ProductDE (Entitiy)
-- ProductEN (Entitiy)

Note: All CustomerX entities use the same "customer" table and all
      ProductX entities use the same "product" table. The only differ
      by business logic.

      Simplified example:
      CustomerDE->getFullName() --> "Herr Peter Mueller"
      CustomerEN->getFullName() --> "Mr. P. Mueller"

Example for a sub class called "CustomerUK" (Enitity) which inherits
from "Customer" (MappedSuperClass). Because Customer has a many to many
association to Products, by default CustomerUK would have too. With this
patch this can be changed by applying the following to CustomerUK:

/**
 * @ORM\Entity
 * @ORM\Table(name="customer")
 * @ORM\AssociationOverrides({
 *      @ORM\AssociationOverride(name="products",
 *          targetEntity="MyProductUK"
 *      )
 * })
 */

 Special thanks so Danijel B.

Added test cases for [DDC-2042]
df1dbfb
@doctrinebot
Collaborator

Hello,

thank you for positing this Pull Request. I have automatically opened an issue on our Jira Bug Tracker for you with the details of this Pull-Request. See the Link:

http://doctrine-project.org/jira/browse/DDC-2373

@FabioBatSilva

HI @spezifanta

During the development of AssociationOverride/AttributeOverride
we discussed about support type changes and we decide does not support this.

IMHO This is a object-orientation mistake.

Could you explain you use case please ?

@spezifanta

Yes, I will try to explain my use case.

The key words may be: "optinal sub class inheritance without modifying it's parent".

I have a Customer and Product class. Both are "SuperMappedClass"es.
Customer and Product have a ManyToMany Association.
Now I'll have a CustomerUK, CustomerDE, which both inherit from Customer and a ProductUK and ProductDE, which, of course, inherit from Product.
Those sub classes only differ by business logic.
By default a CustomerUK and CustomerDE would still have relationship to Product.
But I need CustomerUK to point to ProductUK and CustomerDE to point to ProductDE, without, and here comes the "little extra", the need of modifiying the parent suppermapped classes (I dont need/want a discriminator map in my parent classes, as the are in a different bundle and shall not be edited, because they are maintained by different developer)

And this needs to be optional too so that I can have a CustomerNL and CustomerPL both pointing to ProductEU and a CustomerFR that sill uses the default Product.

Does that makes sense?

Anyway, having the possibility to override a targetEntity in a sub class solves this problem for me.

PS: Just to clarify. This is not a "new" feature request (even though Jira started one). I tryed to help and solve [http://www.doctrine-project.org/jira/browse/DDC-2042], which is currently assigned to Benjamin Eberlei and was reported by Charles Rouillon on 26/Sep/12 8:57 AM.

@stof

@spezifanta For this particular use case, I would use a listener on the loadMetadata event rather than making it possible to change the type

@Padam87

@spezifanta Interesting... i can't for the life of my figure out why you need to have this kind of separation.
Shipping, Invoicing, Translation ... all these issues have much easier solutions than this.

I'm curious, please explain :)

@beberlei
Owner

Your inheritance is wrong. The right way would be something like Category -> Customer + Category -> Product, so you can get rid of the DE, EN. For Example your model fails when a UK customer wants to buy in the DE shop for some reason.

@beberlei beberlei closed this
@spezifanta

@beberlei Hrhr, it is not that easy to be honest. Or at least I could not find a simpler solution :>

@stof I used a listener before I realized that this would only be a esay patch for doctrine. I found the idea of using a listener on someone's blog, who needed to override targetEntities as well ;)

But I should be absolutely straight with you guys. I have Project ("MyProject") which is a kind of API wrapper of multiples providers. It shall use the composer one day to get updated.
Now I have users that might need to override a entitis' business logic or association's of entities for their needs.

overriede

Take a quick note at the targetEntity of OtherCustomerOverrideBundle\MyEntity (on the left, green box)
While overriding it's foobar targetEntity it can use something like this:

$myEntity = $em->find('OtherCustomOverrideBundle\MyEntity', 1);

$myEntity->randomMethod();                        // OtherCustomOverrideBundle\MyEntity (geen box)
$myEntity->getFoobar()->first()->setMyName() ;    // ACustomerOverrideBundle\MyFoobar (orange box)
$myEntity->getFoobar()->first()->otherMethods();  // MyCoreBundle\Foobar (gray box)

Because I don't know my imaginary User1 and UserN, I can't predict which entities' methods the might want to override or what targetEntities they want to use. At same time I can not allow them to modify files in my CoreBundle as this will get updated at some point later (we probably all agree on that changing files in a "vendor" directory is a bad idea).

So by extending the existing AssociationOverride by 5 lines (PR includes extension for XML, YML too) I can now allow Users to inherit my or other's entitie's logic and association in their own entity class without them needing to modify my files, and me without worrying to overwrite their code when rolling out a new release.

I hope I could work out how important the separation of my and my users' entities' logic is and what flexibility this PR would bring to Doctrine 2. I am happy to answer any questions.

@AlexanderParker

I also would like something like this but with a different use case. I have many companies that share a core set of entities. As I am using Symfony, these live in a core bundle, but that's just a convenience of Symfony. The entities in the core bundle have relationships with each other.

Child bundles extend the core entities, and we would like to change the relationships to point to child versions of each entity. Like this:

image

All entities in the child groups extend from the base group. CompanyAPackage in the orange group and CompanyBpackage in the blue group specify a child entity in their respective relationships.

In this way, I'll be able to share among many companies the core fields and functionality, minimising duplication of code and data.

In PHP, if I set up the child / core relationships as joined entities, I can redeclare the private properties of the child classes and specify different target entities in the child classes; doctrine doesn't throw any errors when I do this. It doesn't work though. The changed relationships don't reflect in the schema and the generated stubs break PHP's type hinting contracts (as I don't think Doctrine expects sub-classes to redeclare properties).

I do get an error (which is good!) when I use a mapped superclass for the base entities, perhaps this exception could also be thrown in the previous situation I mentioned?

image

In any case, being able to specify a child target entity in a subclass is would satisfy the desired structure perfectly. As it stands, I won't be able to use inheritance to share common functionality.

If it is not possible or considered "bad" to override target entities, I am curious as to why? What are the alternative approaches I could use?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 26, 2013
  1. @spezifanta

    [DDC-2042] Added "targetEntity" to AssociationOverride

    spezifanta authored
    Having the possibility to override "targetEntity" lets the user the
    opportunity to change assosciations of sub classes without the need
    of changing it's parent class.
    
    Lets say we have the following classes:
    - Customer (MappedSuperClass)
    -- CustomerDE (Entity)
    -- CustomerEN (Entity)
    - Product (MappedSuperClass)
    -- ProductDE (Entitiy)
    -- ProductEN (Entitiy)
    
    Note: All CustomerX entities use the same "customer" table and all
          ProductX entities use the same "product" table. The only differ
          by business logic.
    
          Simplified example:
          CustomerDE->getFullName() --> "Herr Peter Mueller"
          CustomerEN->getFullName() --> "Mr. P. Mueller"
    
    Example for a sub class called "CustomerUK" (Enitity) which inherits
    from "Customer" (MappedSuperClass). Because Customer has a many to many
    association to Products, by default CustomerUK would have too. With this
    patch this can be changed by applying the following to CustomerUK:
    
    /**
     * @ORM\Entity
     * @ORM\Table(name="customer")
     * @ORM\AssociationOverrides({
     *      @ORM\AssociationOverride(name="products",
     *          targetEntity="MyProductUK"
     *      )
     * })
     */
    
     Special thanks so Danijel B.
    
    Added test cases for [DDC-2042]
This page is out of date. Refresh to see the latest.
View
7 lib/Doctrine/ORM/Mapping/AssociationOverride.php
@@ -50,4 +50,11 @@
* @var \Doctrine\ORM\Mapping\JoinTable
*/
public $joinTable;
+
+ /**
+ * The target entitiy that maps the relationship.
+ *
+ * @var string
+ */
+ public $targetEntity;
}
View
4 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -1976,6 +1976,10 @@ public function setAssociationOverride($fieldName, array $overrideMapping)
$mapping['joinTable'] = $overrideMapping['joinTable'];
}
+ if (isset($overrideMapping['targetEntity'])) {
+ $mapping['targetEntity'] = $overrideMapping['targetEntity'];
+ }
+
$mapping['joinColumnFieldNames'] = null;
$mapping['joinTableColumns'] = null;
$mapping['sourceToTargetKeyColumns'] = null;
View
4 lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -403,6 +403,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$override['joinTable'] = $joinTable;
}
+ if ($associationOverride->targetEntity) {
+ $override['targetEntity'] = $associationOverride->targetEntity;
+ }
+
$metadata->setAssociationOverride($fieldName, $override);
}
}
View
4 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
@@ -548,6 +548,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$override['joinTable'] = $joinTable;
}
+ if ($overrideElement->targetEntity) {
+ $override['targetEntity'] = $overrideElement->targetEntity;
+ }
+
$metadata->setAssociationOverride($fieldName, $override);
}
}
View
4 lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
@@ -553,6 +553,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$override['joinTable'] = $joinTable;
}
+ if (isset($associationOverrideElement['targetEntity'])) {
+ $override['targetEntity'] = $associationOverrideElement['targetEntity'];
+ }
+
$metadata->setAssociationOverride($fieldName, $override);
}
}
View
12 tests/Doctrine/Tests/Models/DDC2042/DDC2042Bar.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Doctrine\Tests\Models\DDC2042;
+
+/**
+* @Entity
+*/
+class DDC2042Bar
+{
+ /** @Id @Column(type="string") */
+ private $id;
+}
View
12 tests/Doctrine/Tests/Models/DDC2042/DDC2042Baz.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Doctrine\Tests\Models\DDC2042;
+
+/**
+* @Entity
+*/
+class DDC2042Baz
+{
+ /** @Id @Column(type="string") */
+ private $id;
+}
View
17 tests/Doctrine/Tests/Models/DDC2042/DDC2042ExampleEntityWithOverride.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Doctrine\Tests\Models\DDC2042;
+
+/**
+ * @Entity
+ *
+ * @AssociationOverrides({
+ * @AssociationOverride(name="bar",
+ * targetEntity="DDC2042Baz"
+ * )
+ * })
+ */
+class DDC2042ExampleEntityWithOverride
+{
+ use DDC2042ExampleTrait;
+}
View
11 tests/Doctrine/Tests/Models/DDC2042/DDC2042ExampleEntityWithoutOverride.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Doctrine\Tests\Models\DDC2042;
+
+/**
+ * @Entity
+ */
+class DDC2042ExampleEntityWithoutOverride
+{
+ use DDC2042ExampleTrait;
+}
View
23 tests/Doctrine/Tests/Models/DDC2042/DDC2042ExampleTrait.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Doctrine\Tests\Models\DDC2042;
+
+/**
+ * Trait class
+ */
+trait DDC2042ExampleTrait
+{
+ /** @Id @Column(type="string") */
+ private $id;
+
+ /**
+ * @Column(name="trait_foo", type="integer", length=100, nullable=true, unique=true)
+ */
+ protected $foo;
+
+ /**
+ * @OneToOne(targetEntity="DDC2042Bar", cascade={"persist", "merge"})
+ * @JoinColumn(name="example_trait_bar_id", referencedColumnName="id")
+ */
+ protected $bar;
+}
View
18 tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php
@@ -232,6 +232,24 @@ public function testAttributeOverridesMappingWithTrait()
$this->assertArrayHasKey('example_trait_bar_id', $metadataWithoutOverride->associationMappings['bar']['joinColumnFieldNames']);
$this->assertArrayHasKey('example_entity_overridden_bar_id', $metadataWithOverride->associationMappings['bar']['joinColumnFieldNames']);
}
+
+ /**
+ * @group DDC-2042
+ */
+ public function testAttributeOverridesMappingWithTargetEntity()
+ {
+ if (!version_compare(PHP_VERSION, '5.4.0', '>=')) {
+ $this->markTestSkipped('This test is only for 5.4+.');
+ }
+
+ $factory = $this->createClassMetadataFactory();
+
+ $metadataWithoutOverride = $factory->getMetadataFor('Doctrine\Tests\Models\DDC2042\DDC2042ExampleEntityWithoutOverride');
+ $metadataWithOverride = $factory->getMetadataFor('Doctrine\Tests\Models\DDC2042\DDC2042ExampleEntityWithOverride');
+
+ $this->assertEquals('Doctrine\Tests\Models\DDC2042\DDC2042Bar', $metadataWithoutOverride->associationMappings['bar']['targetEntity']);
+ $this->assertEquals('Doctrine\Tests\Models\DDC2042\DDC2042Baz', $metadataWithOverride->associationMappings['bar']['targetEntity']);
+ }
}
/**
Something went wrong with that request. Please try again.