Doctrine2: mapeo eficiente de clases heredadas

I've a problem figuring out how to configure the mapping of my classes with Doctrine2.

Let say I've these tables:

Address table 
--------------------- 
- id 
- civic_no 
- road 
- state 
- country 

PersonnalAddress table 
--------------------- 
- id 
- base_address_id 
- type 
- is_primary 

BusinessAddress table 
--------------------- 
- id 
- base_address_id 
- business_name 
- shipping_phone 
- is_primary 

And thoses PHP objects:

class Address{}
class BusinessAddress extends Address{}
class PersonalAddress extends Address{}

Considering the following requirements:

  • An address can exist by itself (the Address class is not abstract)
  • A personalAddress and a businessAddress can have the very same address data
  • If I delete or edit the address, it has an impact on all the business or personal address that are inherited from it.
  • I don't want any data duplication in the database (this is a requirement of the 2nd normal form)
  • Proxy methods mean code duplication, I prefer not to have any.
  • Magic methods are not good in term of testability, I prefer not to have any.

To better illustrate the problem, I expect the data in the data base to look like:

Address table:
id | civic_no | road | state | country
 1        123   test      qc        ca

PersonnalAddress table:
id | base_address_id | type | is_primary 
 1                 1      A            0
 2                 1      B            1

BusinessAddress table:
id | base_address_id | business_name | shipping_phone | is_primary 
 1                 1       chic choc          1231234            1

What would be the best strategy to implement a solution that match theses requirements ?

preguntado el 08 de noviembre de 11 a las 18:11

2 Respuestas

Ok this is a bit of a long one but I think it covers all your bases, if you have any questions then feel free to ask.

This comes with a caveat that I don't know if you can do Many-To-One on a MappedSuperclass. If that isn't possible then you may be able to use Class Table Inheritance instead. Give it a try and tell us if it works.

Keep in mind I pushed this code out pretty quickly, it is untested so it may not be correct but hopefully you'll get the idea of how it works.

Aquí vamos!

Interface to make it easier to say "what is an address" without making abstract classes then overriding methods and causing the "bad design" feeling ;)

interface Address {

    function getCivicNo();

    function getRoad();

    function getState();

    function getCountry();
}

Abstract Entity which you don't really need but if you dont use it you need to duplicate the ID code.

/**
 * @ORM\MappedSuperclass
 */
abstract class AbstractEntity {

    /**
     * Entity ID column.
     * 
     * @var integer
     * 
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    private $id;

    public function getId() {
        return $id;
    }

}

BasicAddress which you can store alone or have linked to a "ComplexAddress"

/**
 * @ORM\Entity
 */
class BasicAddress extends AbstractEntity implements Address {

    /** @ORM\Column() */
    private $road;

    public function getRoad() {
        return $this->road;
    }

    // etc etc

}

"ComplexAddress" is just here to let you re-use the code for delegating calls to a basic address.

/**
 * @ORM\MappedSuperclass
 */
abstract class ComplexAddress extends AbstractEntity implements Address {

    /** @ORM\Many-To-One(targetEntity="BasicAddress")
    private $basicAddress;

    public function __construct(BasicAddress $basicAddress) {
        $this->basicAddress = $basicAddress;
    }

    public function getRoad() {
        return $this->basicAddress->getRoad();
    }

    // other methods for implementing "Address" just delegate to BasicAddress

}

PublicAddress

/**
 * @ORM\Entity()
 */
class PersonalAddress extends ComplexAddress {

    /** @ORM\Column(type="boolean") */
    private $isPrimary;

    public function isPrimary() {
        return $isPrimary;
    }

    // other personal address methods here

}

Dirección de Negocios

/**
 * @ORM\Entity()
 */
class BusinessAddress extends ComplexAddress {

    /** @ORM\Column() */
    private $businessName;

    public function getBusinessName() {
        return $this->businessName;
    }

    // other business address methods here

}

Edit: Just noticed I forgot to put cascade parameters for deletion, you might need to handle this directly though - when a BasicAddress is deleted also delete the other addresses that use it.

respondido 11 nov., 11:03

I think it might work, the manual says: "This means that One-To-Many associations are not possible on a mapped superclass at all.", and the other 2 types of inheritance wouldn't work as you need a discriminator field that render impossible for 2 different types to have the same parent. - FMaz008

En lugar de extender el Dirección class, you'll have to create a Uno a muchos relationship on the Dirección y un ManyToOne relationship on the PersonalAddress y Dirección de Negocios. Algo como esto:

<?php

// ...

use Doctrine\Common\Collections\ArrayCollection;

// ...
class Address
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="PersonalAddress", mappedBy="address")
     */
    private $personalAddresses;

    /**
     * @ORM\OneToMany(targetEntity="BusinessAddress", mappedBy="address")
     */
    private $businessAddresses;

    public function __construct()
    {
        $this->personalAddresses = new ArrayCollection();
        $this->businessAddresses = new ArrayCollection();
    }

    // ...
}

And, for the child classes:

<?php

// ...

// ...
class PersonalAddress
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Address", inversedBy="personalAddresses")
     * @ORM\JoinColumn(name="base_address_id", referencedColumnName="id")
     */
    private $address;

    // ...
}

respondido 09 nov., 11:16

Interesting, but why do my Address object has to know about personalAddress and businessAddress ? Also, how will I access the Address method from the PersonnalAddress class without using proxy method ? - FMaz008

You mean, how do can you access the Address clase de la PersonalAddressClass? Create getters and setters running doctrine:generate:entities y tu puedes hacer $personalAddress->getAddress(). - Alessandro Desantis

That was my first implementation attempt. From a PHP perspective, it's really not what I want. The person who'll use the class now have to handle the object has 2 objects. So to be able to use the object has a unified object, I would have to create proxy method, like in personAddress, businessAddress, etc... that look like: public function getStreet(){ return $this->address->getRoad(); } and that's a very bad practice (event if it seem well used in the doctrine community) - FMaz008

Why? You can just write: $businessAddress->getAddress()->getRoad(). - Alessandro Desantis

I don't want the users who use the code to have to understand the logics behind the object. They use a business address, that's it. That's why I want to use inheritance, and now relations, as you could do 100% of the OO needs with relations only, and never use extends, but that would not be nice and intuitive. ;) - FMaz008

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.