8

How do I create traditional polymorphic relationships with Doctrine 2?

I have read a lot of answers that suggest using Single Table Inheritance but I can't see how this would help in my situation. Here's what I'm trying to do:

I have some utility entities, like an Address, an Email and a PhoneNumber.

I have some 'contactable' entities, like a Customer, Employer, Business. Each of these should contain a OneToMany relationship with the above utility entities.

Ideally, I'd like to create an abstract base class called 'ContactableEntity' that contains these relationships, but I know it is not possible to put OneToMany relationships in mapped superclasses with doctrine-- that's fine.

However, I am still at a loss at how I can relate these without massive redundancy in code. Do I make Address an STI type, with a 'CustomerAddress' subclass that contains the relationship directly to a Customer? Is there no way to reduce the amount of repetition?

Tyler Sommer
  • 428
  • 4
  • 12
  • related question http://stackoverflow.com/questions/13124225/doctrine2-onetomany-on-mapped-superclass/20000096 – pleerock Nov 15 '13 at 11:31

2 Answers2

5

Why not just make your base ContactableEntity concrete?

EDIT:

Just did a few experiments in a project I've done that uses CTI. I don't see any reason that the same strategy wouldn't work with STI.

Basically, I have something like:

/**                                                                                                                                                                                                                                                                                      
 * Base class for orders.  Actual orders are some subclass of order.                                                                                                                                                                                                                     
 *                                                                                                                                                                                                                                                                                       
 * @Entity                                                                                                                                                                                                             
 * @Table(name="OOrder")                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
 * @InheritanceType("JOINED")                                                                                                                                                                                                                                                            
 * @DiscriminatorColumn(name="discr", type="string")                                                                                                                                                                                                                                     
 * @DiscriminatorMap({"CAOrder" = "CAOrder", "AmazonOrder" = "AmazonOrder"})                                                                                                                                                                                                                                                                                                                                                                                      
 */
abstract class Order {
    /**           
     * CSRs can add notes to orders of any type                                                                                                                                                                                                                                                          
     * @OneToMany(targetEntity = "OrderNote", mappedBy = "order", cascade={"all"})                                                                                                                                                                                                         
     * @OrderBy({"created" = "ASC"})                                                                                                                                                                                                                                                         
     */
    protected $notes;

    // ...
}

/**                                                                                                                                                                                                                                                                                      
 * @Entity                                                                                                                                                                                                                                                                               
 */
class AmazonOrder extends Order {

  /**                                                                                                                                                                                                                                                                                    
   * @Column(type="string", length="20")                                                                                                                                                                                                                                                 
   */
  protected $amazonOrderId;

  // ...

}

/**                                                                                                                                                                                                                                                                                      
 * @Entity                                                                                                                                                                                                                                                                               
 */
class OrderNote {
    // ...

    /**                                                                                                                                                                                                                                                                                    
     * @ManyToOne(targetEntity="Order", inversedBy="notes")                                                                                                                                                                                                                                
     */
    protected $order;

    // ...
}

And it seems to work exactly as expected. I can get an OrderNote, and it's $order property will contain some subclass of Order.

Is there some restriction on using STI that makes this not possible for you? If so, I'd suggest moving to CTI. But I can't imagine why this wouldn't work with STI.

timdev
  • 61,857
  • 6
  • 82
  • 92
  • Making it concrete would mean all my other entities would need to use Class Table Inheritance. It is not the fact that the class is abstract that keeps me from using it in an OneToMany, rather that it is a MappedSuperclass. However, even were I to drop the AbstractContactableEntity, I still am not able to have polymorphic relationships from a single kind of 'Address' entity to multiple different other entities- as far as I can tell. – Tyler Sommer Oct 24 '11 at 19:27
  • 1
    Updated my answer - I'm not sure why you say you'd need to use Class-Table Inheritance. I did some tests and (using CTI), I can have an abstract base class with one-to-many relationships that subclasses inherit, and it seems to work fine. Don't see why it wouldn't work with STI, too -- but if it doesn't, I'm very curious why not. – timdev Oct 24 '11 at 20:56
  • Thanks for the example. My setup is similar to yours, but it is not validating: http://pastebin.com/q28MUdkk The error message I am getting when running orm:validate-schema: `* The association Address#contactable refers to the inverse side field AbstractContactableEntity#addresses which does not exist. * The referenced column name 'id' does not have a corresponding field with this column name on the class 'AbstractContactableEntity'.` – Tyler Sommer Oct 25 '11 at 12:55
  • 2
    That's because you're using MappedSuperClass -- make it an Entity instead (it can still be an abstract class), and I suspect you'll be good. – timdev Oct 25 '11 at 16:22
  • 1
    Hahaha I feel like such an idiot. That worked. Thank you, kind sir. – Tyler Sommer Oct 25 '11 at 18:02
  • From the docs: There is a general performance consideration with Single Table Inheritance: If the target-entity of a many-to-one or one-to-one association is an STI entity, it is preferable for performance reasons that it be a leaf entity in the inheritance hierarchy, (ie. have no subclasses). Otherwise Doctrine CANNOT create proxy instances of this entity and will ALWAYS load the entity eagerly. https://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#performance-impact – Leevi Graham Oct 22 '15 at 12:36
  • @LeeviGraham while it's important to keep that in mind, it's not directly relevant here since OP's ContactableEntities aren't the target of many-to-one or one-to-one association. – timdev Oct 23 '15 at 02:50
  • Given "Customer" contains a oneToMany to "Address " wouldn't the inverse side be a manyToOne and therefore if the "Address" was loaded it would eagerly load the "Customer". There's Probably not much chance of loading the "Address" without the "Customer". – Leevi Graham Oct 23 '15 at 06:01
0

If the contactable entity shall be abstract (@MappedSuperclass) you'll need to use the ResolveTargetEntityListener provided by Doctrine 2.2+.

It basically allows you to define a relationship by specifying an interface instead of a concrete entity. (Maybe you want to define/inherit several interfaces as you speak of multiple "contactables"). For instance you then can implement the interface in your abstract class or concrete class. Finally you'll need to define/associate the concrete class (entity) to the related interface within the config.yml

An example can be found in the Symfony docs: http://symfony.com/doc/current/cookbook/doctrine/resolve_target_entity.html

Marc Juchli
  • 2,240
  • 24
  • 20