1

I didn't find informations about that.

A lot of peoples tell me that is a non-sense to put @Entity annotation on an abstract class. From my point of view when I make a PHP abstract class I mainly expect no PHP code can create instances using new operator.

Some peoples tell me to use @MappedSuperClass on classes like ChildrenAbstract but unless I remove @Entity annotation that seems useless because I sometime need mappings that are not allowed with @MappedSuperClass.

A lot of peoples asks me why so here is basically why :

ParentAbstract.php

/**
 * @ORM\Table(name="ParentAbstract")
 * @ORM\Entity(repositoryClass="App\Repository\ParentAbstractRepository")
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap({"childrenAbstract" = "ChildrenAbstract", "concreteA" = "ConcreteA", "concreteB" = "ConcreteB"})
 */
abstract class ParentAbstract
{
   ...
}

ChildrenAbstract.php

/**
 * @ORM\Table(name="ChildrenAbstract")
 * @ORM\Entity(repositoryClass="App\Repository\ChildrenAbstractRepository")
 */
abstract class ChildrenAbstract extends ParentAbstract
{   
   /** * @ORM\Column(type="string", nullable=true) */
   private $picture;
}

ConcreteA.php

/**
 * @ORM\Table(name="ConcreteA")
 * @ORM\Entity(repositoryClass="App\Repository\ConcreteARepository")
 */
class ConcreteA extends ChildrenAbstract
{   
   ...
}

ConcreteB.php

/**
 * @ORM\Table(name="ConcreteB")
 * @ORM\Entity(repositoryClass="App\Repository\ConcreteBRepository")
 */
class ConcreteB extends ChildrenAbstract
{   
   ...
}

ChildrenAbstractController.php

class ChildrenAbstractController extends AbstractController
{   
   /**
     * @Route("/home/childrenabstract/get/picture/{id}", name="get_childrenabstract_picture")
     */
   public function getChildrenAbstractPicture(Request $request, int $id) : Response
   {
       $childrenAbstract = $this->getDoctrine()->getRepository(ChildrenAbstract::class)->findById($id);

       if(!$childrenAbstract)
       {
           throw new \Exception("childrenAbstract don't exists !");
       }

       $response = new BinaryFileResponse("../path/".$childrenAbstract->getId()."/".$childrenAbstract->getPicture());
       $response->headers->set('Content-Type', 'image/' . 'png');
       $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);

       return $response;
   }    

ConcreteAFront.html.twig

{% for concreteA in container.concreteAs %}
   <td>{{ form_label(form.picture) }}</td><td><img src="{{ path('get_childrenabstract_picture', {id:concreteA.id}) }}" /></td>
{% endfor %}

ConcreteBFront.html.twig

{% for concreteB in container.concreteBs %}
   <td>{{ form_label(form.picture) }}</td><td><img src="{{ path('get_childrenabstract_picture', {id:concreteB.id}) }}" /></td>
{% endfor %}

I need a "generic" route to interact with some part of concrete structure and sometime mappings in AbctractChildren.php are more complex with @ORM\ManyToOne, @ORM\ManyToMany, ...

The point is that is perfectly working. Or that seems to perfelctly works. I need to be sure Doctrine2 is designed to support that way or if it's a bad practice to avoid.

Nicolas
  • 167
  • 8

1 Answers1

2

TL;DR: Avoid placing an entity annotation on an abstract class. First, its unusual and may confuse future maintainers. Second, it goes against how Doctrine expects to work and may, as a result, stop working in a future Doctrine release.

Good question! Let's go to the docs. From the Basic Mapping section:

... Doctrine only knows about your entities because you will describe their existence and structure using mapping metadata, which is configuration that tells Doctrine how your entity should be stored in the database.

With no additional information, Doctrine expects the entity to be saved into a table with the same name as the class in our case Message.

and from the Inheritance Mapping section:

A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.

So, Doctrine's expecting two things:

  1. Classes annotated as @Entity will be manipulated as objects, each object representing a row of data in a table.
  2. Classes annotated as @MappedSuperclass will NOT be manipulated as objects and will NOT represent a row of data

An abstract class cannot fit into category #1 by definition, so it must only fit into category #2. That's the theory, anyway. Practically, you're angling for re-use and that's a job for MappedSuperclass. Do use it, that's why Doctrine makes that possible!

You indicated you had experienced an issue with implementing MappedSuperclass and relationships, and I'm guessing you were encountered an error like:

[Doctrine\ORM\Mapping\MappingException] It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass 'App...\AbstractFoo#thing'.

This happens because of a language implementation limitation and can be fixed by opening your properties from private to protected.

If you encountered a different issue, please post it. If Mappedsuperclass can't be used to handle your use case, then either your particular use case is quite unique, or it's a bug in Doctrine. Either way, it deserves further evaluation.

bishop
  • 37,830
  • 11
  • 104
  • 139
  • 2
    As an adition to this answer, might want to mention that you **do not need to declare discriminator map**. [In the docs](https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/inheritance-mapping.html): `If no discriminator map is provided, then the map is generated automatically. The automatically generated discriminator map contains the lowercase short name of each class as key.` - Particularly useful when using an abstract class/MappedSuperClass, as the parent should have no idea about any child. – rkeet Dec 25 '19 at 16:17