5

I have some entites with common relations and attributes. So, I want to simplify my schema using inheritance mapping.

I created a BaseData mappedsuperclass, and make my other entities expand it. This BaseData class has the common relations I need in each entity.

It works with many-to-one relation, like

/**
 * @ORM\MappedSuperclass
 */
class BaseData
{

    /**
    * @ORM\ManyToOne(targetEntity="Service")
    * @ORM\JoinColumn(name="service_id", referencedColumnName="id")
    */  
    protected $service;

But it become a little bit more tricky with self-referencing.

For instance, since I want to create a parent reference, I tried that :

/**
 * @ORM\MappedSuperclass
 */
class BaseData
{

    /**
    * @ORM\ManyToOne(targetEntity="BaseData")
    * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
    */
    protected $parent;

Obviously, it lead to a TableNotFoundException when I try to query this entity : QLSTATE[42S02]: Base table or view not found: 1146 Table 'project.base_data' doesn't exist.

So, I tried AssociationOverrides, but it seems that doesn't allow to change the Target Entity.

So, is there a way to build some self-reference on a MappedSuperclass ? And by the way, does it even make sense ?

Many thanks in advance !

Update

Here is the anwser :

I defined the protected $parent and protected $children in my BaseData mappedSuperClass as planned. I annotated them with other information I need. eg :

/**
 * @ORM\MappedSuperclass
 */
class BaseData
{

    /**
    * @Datagrid\Column(field="parent.id", title="datagrid.parent_id", visible=false, safe=false)
    * @Serializer\Expose
    * @Serializer\Groups({"foo"})
    */
    protected $parent;

    /**
     * @Serializer\Expose
     * @Serializer\Groups({"elastica"})
     */
    protected $children;

Then, I add the ORM relation with the event loadClassMetadata.

/**
 * @param LoadClassMetadataEventArgs $eventArgs
 */
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
    // the $metadata is all the mapping info for this class
    $classMetadata  = $eventArgs->getClassMetadata();
    $reflObj = new \ReflectionClass($classMetadata->name);
    if($reflObj) {
        if ($reflObj->isSubclassOf('CoreBundle\Entity\BaseData')) {
            $fieldMapping = array(
                'targetEntity'  => $classMetadata->name,
                'fieldName'     => 'parent',
                'inversedBy'    => 'children',
                'JoinColumn'    => array(
                    'name'                  => 'parent_id',
                    'referencedColumnName'  => 'id',
                    'nullable'              => true,
                    'onDelete'              => 'SET NULL',
                ),
            );

            $classMetadata->mapManyToOne($fieldMapping);


            $fieldMapping = array(
                'fieldName'     => 'children',
                'targetEntity'  => $classMetadata->name,
                'mappedBy'      => 'parent',
            );
            $classMetadata->mapOneToMany($fieldMapping);
        }
    }
}

Register the event, and that's it.

Now, every class which extends the BaseData superClass get the relation. For instance, php app/console doctrine:generate:entities MyBundle will generates the following code inside the SubClass entity :

/**
 * Set parent
 *
 * @param \MyBundle\Entity\Subclass $parent
 *
 * @return Subclass
 */
public function setParent(\MyBundle\Entity\Subclass $parent = null)
{
    $this->parent = $parent;

    return $this;
}

/**
 * Get parent
 *
 * @return \MyBundle\Entity\Subclass
 */
public function getParent()
{
    return $this->parent;
}

/**
 * Add child
 *
 * @param \MyBundle\Entity\Subclass $child
 *
 * @return Subclass
 */
public function addChild(\MyBundle\Entity\Subclass $child)
{
    $this->children[] = $child;

    return $this;
}

/**
 * Remove child
 *
 * @param \MyBundle\Entity\Subclass $child
 */
public function removeChild(\MyBundle\Entity\Subclass $child)
{
    $this->children->removeElement($child);
}

/**
 * Get children
 *
 * @return \Doctrine\Common\Collections\Collection
 */
public function getChildren()
{
    return $this->children;
}
TiPi
  • 330
  • 4
  • 19
  • You could do self-reference only for child classes as MappedSupperclass is not itself an entity. From the docs: A mapped superclass cannot be an entity, it is not query-able and persistent relationships defined by a mapped superclass must be unidirectional (with an owning side only). This means that One-To-Many associations are not possible on a mapped superclass at all. Furthermore Many-To-Many associations are only possible if the mapped superclass is only used in exactly one entity at the moment. – Ilya Yarkovets Nov 30 '15 at 10:51
  • I guessed that indeed, but my goal was to make the self-reference working for each child classes. I mean, by writing `targetEntity="BaseData"`, I was hoping that every child classes was going to use its own name. So, I guess, I have to make the parent self-reference in each child classes manually, isn't it ? And thanks for your help. – TiPi Nov 30 '15 at 14:28
  • 1
    No problem! As @MappedSuperclass is not an entity itself, yes, I guess, you could only do this in child classes. p.s. IMHO, @MappedSuperclass is very useful for all kind of dictionaries - in the last project, for example, I had one BaseDictionaryDocument, with two fields id and sysName. And all dictionaries were just looking like `class BlahBlahDocument extends BaseDictionaryDocument {}` and, of course, an annotation with table/collection name at the top. – Ilya Yarkovets Nov 30 '15 at 14:47
  • Hm, so I guess I'll do it manually... I'd like to set that on a superclass, so I'm going to dig a little bit more in other direction. Thanks again. – TiPi Nov 30 '15 at 15:40

1 Answers1

2

You can remove the mapping @ORM\ManyToOne(targetEntity="BaseData") and create an event listener on the event loadClassMetadata. (I didn't test the following code, this is just a starting point) Something like this:

class TestEvent
{
    public function loadClassMetadata(\Doctrine\ORM\Event\LoadClassMetadataEventArgs $eventArgs)
    {
        $classMetadata = $eventArgs->getClassMetadata();
        $class = $classMetadata->getName();
        $fieldMapping = array(
            'fieldName' => 'parent',
            'targetEntity' => $class,
        );
        $classMetadata->mapManyToOne($fieldMapping);
    }
}

One important thing to notice is that a listener will be listening for all entities in your application.

See Doctrine docs about events And How to register event listener in symfony2

Mohamed Ramrami
  • 12,026
  • 4
  • 33
  • 49