0

The following script comes from https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/inheritance-mapping.html#mapped-superclasses, and was only changed to include a second sub-class. It is my understanding that MappedSuperclassBase cannot exist by itself but must be extended by one and only one sub-class (i.e. either EntitySubClassOne or EntitySubClassTwo), and is the same concept as supertype/subtype for SQL. Agree?

How is a super/sub type defined using either YAML or XML instead of annotation mapping?

<?php
/** @MappedSuperclass */
class MappedSuperclassBase
{
    /** @Column(type="integer") */
    protected $mapped1;
    /** @Column(type="string") */
    protected $mapped2;
    /**
     * @OneToOne(targetEntity="MappedSuperclassRelated1")
     * @JoinColumn(name="related1_id", referencedColumnName="id")
     */
    protected $mappedRelated1;

    // ... more fields and methods
}

/** @Entity */
class EntitySubClassOne extends MappedSuperclassBase
{
    /** @Id @Column(type="integer") */
    private $id;
    /** @Column(type="string") */
    private $name;

    // ... more fields and methods
}

/** @Entity */
class EntitySubClassTwo extends MappedSuperclassBase
{
    /** @Id @Column(type="integer") */
    private $id;
    /** @Column(type="string") */
    private $name;

    // ... more fields and methods
}
user1032531
  • 24,767
  • 68
  • 217
  • 387
  • On your link, scroll down to here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/inheritance-mapping.html#association-override ;-) – rkeet Feb 05 '19 at 14:54
  • @rkeet Unless I am mistaken, that section only describes association overrides, and the part in question is left out and described as `//other fields mapping`. – user1032531 Feb 05 '19 at 15:01
  • Your question asks "how to make using Yaml / XML". If you go to that link specifically, the code example there has tabs up top allowing you to switch language. If you read that code you see "how" to map the requested `MappedSuperClass` there. (Yaml: `type: mappedSuperclass`, XML: ``) – rkeet Feb 05 '19 at 15:10
  • @rkeet But it doesn't show the equivalent of `@OneToOne(targetEntity="MappedSuperclassRelated1")` and `@JoinColumn(name="related1_id", referencedColumnName="id")`, granted, this is not too difficult to convert. More importantly, it doesn't explain how `EntitySubClass` knows that it is associated with `MappedSuperclassBase`, and that is the part I am struggling on. Thanks – user1032531 Feb 05 '19 at 18:58
  • If `Worker` extends `Person` (superclass), then you have 1 Entity: Worker. Creating a relation to `Worker` (ie from `Paycheck`), links to just Worker. The superclass `Person` just creates a base set of properties/defaults that are used in Worker. – rkeet Feb 06 '19 at 08:05
  • I think you may be mixing up a `MappedSuperClass` with a `Discriminator` class. The first provides properties/defaults for child-class, but is never an Entity by itself. The latter, discriminated Entity, is a class on its own. – rkeet Feb 06 '19 at 08:07

1 Answers1

2

Based on our comments, I think I see your confusion. Because the docs handle both "MappedSuperclass" and "Discriminator" on the same page, I think you've mixed up their uses in your head. Hopefully this can help you:

  • A MappedSuperclass provides properties/defaults in a re-usable way, but it can never be an Entity by itself. This is comparable to PHP's abstract classes (which cannot be instantiated on their own)
  • A Discriminator provides the ability to "extend" an Entity, making it another Entity. For example, having a Person Entity gives you 1 Entity. This Entity can be extended, for example by Worker and Manager.

A good use-case for a MappedSuperclass would be an AbstractEntity. Every Entity needs an ID, a unique identifier. It also gives you something common to check against in Listeners and such. So, go ahead and create:

/**
 * @ORM\MappedSuperclass
 */
abstract class AbstractEntity
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(name="id", type="integer", options={"unsigned":true})
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;
  
    // getter / setter
}

See how this is both declared abstract and MappedSuperclass?

This is because neither (abstract class and MappedSuperclass) cannot be instantiated on their own. You cannot do $entity = new AbstractEntity() because it's an abstract PHP class. Neither will Doctrine create a separate table for AbstractEntity.

Next, create a Person:

/**
 * @ORM\Entity
 * @ORM\Table(name="persons")
 *
 * @InheritanceType("JOINED")
 * @DiscriminatorColumn(name="discr", type="string")
 */
class Person extends AbstractEntity
{
    /**
     * @var string
     * @ORM\Column(name="name", type="string", length=255, nullable=false)
     */
    protected $name;

    // getter / setter
}

The above, Person, Entity is setup for Class Table Inheritance through the JOINED inheritance type. Meaning that, on the database level, the table persons will be separate from any columns added by other entities, extending Person.

Notice how I did not declare DiscriminatorMap. Below from the docs, highlighted in bold by me:

Things to note:

  • The @InheritanceType, @DiscriminatorColumn and @DiscriminatorMap must be specified on the topmost class that is part of the mapped entity hierarchy.
  • The @DiscriminatorMap specifies which values of the discriminator column identify a row as being of which type. In the case above a value of "person" identifies a row as being of type Person and "employee" identifies a row as being of type Employee.
  • The names of the classes in the discriminator map do not need to be fully qualified if the classes are contained in the same namespace as the entity class on which the discriminator map is applied.
  • 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.

Now, let's create a Worker:

/**
 * @ORM\Entity
 * @ORM\Table(name="workers")
 */
class Worker extends Person
{
    /**
     * @var int
     * @ORM\Column(name="worker_id", type="integer", length=11, nullable=false)
     */
    protected $workerId;

    // getter / setter
}

So, now we've got:

  • MappedSuperclass: AbstractEntity - is not a stand-alone Entity
  • Discriminated: Person - is a stand-alone Entity
  • "normal": Worker - extends Person

Things to note:

  • A MappedSuperclass can not be instantiated. As such: you can not create links/relations to it. Comparable with PHP's abstract class
  • A Discriminated Entity is one which also stands alone and can be used as a normal Entity. You can create relations to and from it, without an issue
  • An Entity extending a Discriminated Entity is an instance of both. In the above code these are both true: $worker instanceof Worker and $worker instanceof Person, because the Worker extends Person. However, $person instanceof Worker will be false!

$workerId = $person->getWorkerId() // generates "method does not exist" fatal

$workerId = $worker->getWorkerId() // generates integer value


Hope that managed to clear stuff up for you. If not, feel free to ask.

Community
  • 1
  • 1
rkeet
  • 3,406
  • 2
  • 23
  • 49
  • Thanks rkeet. On your example, you explicitly defined `class Person extends AbstractEntity`. But if implemented using XML or YAML, wouldn't one NOT start with a PHP class therefore not be able to define this relationship this way? – user1032531 Feb 06 '19 at 11:52
  • You **would** start that way, see earlier link here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/inheritance-mapping.html#association-override - Case where `User` is a MappedSuperclass which is extended by `Admin`. If you follow that, you should be able to get it working. – rkeet Feb 06 '19 at 14:40
  • Ah, I see the source of my confusion. I incorrectly thought that a MappedSuperclass represents SQL supertype/subtype (https://learndatamodeling.com/blog/supertype-and-subtype/). I understood that one could not instantiate a MappedSuperclass, but I did not realize there was no table associated with one! – user1032531 Feb 07 '19 at 10:42
  • Ah ok, the one bit I did not explain in the answer ;-) But again, this is like an `abstract class` vs a `class` ;-) As the first cannot be instantiated, as an Entity it does not have a table. The latter _can_ be instantiated and should have a data store (table) associated. Then there's the discriminated classes, where the "single table inheritance" has the "root" table of the most parent class which gets extended and the class table inheritance where each Entity has it's own table, but is joined to its parent (and thus it's specific table only adds it's added properties). – rkeet Feb 07 '19 at 11:18
  • Thanks again for your help. First time every experimenting with an ORM, and it has been a journey, but is starting to come together. The discriminated classes is what I want. – user1032531 Feb 07 '19 at 12:21
  • Regarding `discriminator column`, I haven't yet figured that part out. If I have an abstract `Person` and a `person` table with primary key `id`, and concrete `Teacher` and `Student` with tables `teacher` and `student` also with primary keys `id` where a `Person` could only be joined to either one `Teacher` or one `Student` using a one-to-one relationship, would I place `@InheritanceType("JOINED")`, `@DiscriminatorColumn(name="id", type="integer")`, and `@DiscriminatorMap({"teacher" = "Teacher", "student" = "Student"}` in `Person`? – user1032531 Feb 07 '19 at 12:27
  • Ah, I believe I am mistaken regarding the `discriminator column`. Current, I do not have a `type` column in `person`, but rely on the `id` join. Maybe I should add one... – user1032531 Feb 07 '19 at 12:37
  • `JOINED` causes a **table per entity**, however, the "first" Entity (e.g. Person) contains just `name` column (and ID primary key), second table (e.g. Worker) contains just `workerId` column (and ID **foreign key**). – rkeet Feb 07 '19 at 12:56
  • `SINGLE_TABLE` causes all "child" entities to _extend_ the original table. So for both Person and Worker (and Boss and Relative) you get a total of **_1_** (one) table! -> id, name, workerId, bossId, relativeId - where all the additional fields are 'null'. – rkeet Feb 07 '19 at 12:57
  • With regards to `@DiscriminiatorMap`, just delete that line (see bold quote in answer). Leave that to Doctrine. Only do `@DiscriminatorColumn(name="discr", type="string")` and `DiscriminatorType` . (fill the last in yourself, I advise `JOINED`) – rkeet Feb 07 '19 at 12:58