Doctrine extension bundle
Entity resolver
The entity resolver is a service to resolve a entity object to a name and on the other hand you can pass the name with an id to receive this entity again.
interface EntityResolverInterface
{
public function getName(object $entity): string;
public function getEntity(int $id, string $name): ?object;
}
Why that is useful? In some cases you need to save the relation to an entity. But you can't serialize it, because this entity might been updated.
To facing that problem you can just save the name and the id of that entity a retrieve it later by the entity resolver.
The entity name could simple the class name. But we also use sylius and therefor the entity class might change because of an update of the software. So it is useful to save an alias for this entity. The entity resolver again will take care of that.
How to use
To use the entity resolver, just inject the service Enhavo\Bundle\DoctrineExtensionBundle\EntityResolver\EntityResolverInterface
.
services:
my_service:
arguments:
- '@Enhavo\Bundle\DoctrineExtensionBundle\EntityResolver\EntityResolverInterface'
Extend
Time by time you have to extend a entity. Doctrine is a powerful ORM and of course this case is covered at all. Doctrine comes with several type of inheritance. The Mapped Superclass, Single- and Multiple table inheritance.
But they have all something in common. You have declare inheritance in the mapping file of entity you want to extend from. This is no problem if you can edit this file. But what if this file live in the vendor directory because it is package?
Then you can only hook into doctrine with a listener and change the mapping information of the entity. This is indeed uncomfortable. So this extension will help you to make it a bit more easy by only adding some meta information to the config.
Because the most use case is to extend a given entity to add a few properties you need, the only supported inheritance type is the single table inheritance. So let's have a deeper look how it works.
Single table inheritance
Imagine we have a simple class Person
with one property id
and name
class Person
{
private $id;
private $name;
// ... Getter and setters
}
Then the resulting table is very easy and straight forward.
+----+---------------+
| id | name |
+====+===============+
| 1 | Bob |
+----+---------------+
But now we will extend our Person
entity and add some more information e.g. gender
class GenderPerson extends Person
{
private $gender;
// ... Getter and setters
}
Because we are using single table inheritance the result will be of course one table, which will be extended by the properties of our GenderPerson
and a further discriminator column, so we know what kind of class we are talking about.
+----+---------------+---------------+----------+
| id | name | gender | discr |
+====+===============+===============+==========+
| 1 | Bob | male | root |
+----+---------------+---------------+----------+
| 2 | Alice | female | app |
+----+---------------+---------------+----------+
Configuration
To get the single table inheritance, we only need to tell EnhavoDoctrineExtensionBundle
that our class GenderPerson
is extending Person
and what is the value of the discr
column. The entity which you will extend from will automatically have the value root
.
Just add following to your config.
enhavo_doctrine_extension:
metadata:
App\Entity\GenderPerson:
extends: App\Entity\Person
discrName: 'app'
Multiple hierarchy
Multiple inheritance are also possible. Just add another config and change the discrName
so it is unique over your hierarchy.
Reference
Polymorphism
Imagine you have a model class that refers to an other class by just knowing it's interface and not the concrete implementation. Of course that's a very common use case in object orientated programming.
Here you can see a simple page class, that contains blocks like a text or a picture.
But if you want to translate this polymorphism into a relational database like mysql, it's not so clear how your table structure will look like.
Normally we are not interested how the sql schema will end up, because we use doctrine as our ORM layer which should take care of it. But in doctrine, we are not able to configure this out of the box. We need some help by extensions. Enhavo has it's own extension for that use case.
Implementations
In general, we have two obvious sql implementations with pros and cons. Let's have look at the page table.
+----+---------------+------------------+
| id | text_block_id | picture_block_id |
+----+---------------+------------------+
| 1 | 1 | NULL |
+----+---------------+------------------+
| 2 | NULL | 1 |
+----+---------------+------------------+
With this structure we can use sql foreign key constraints, because one column is a reference to one specific table. This also gives us the option to use delete cascades. But if we add more and more blocks, the table is growing by adding a new column for each block we add.
Further, if we have a look to the php implementation, doctrine translates each column to a class property. As a result, to encapsulate the data model we need to use if or switch statements for each block.
class Page
{
private $textBlock;
private $pictureBlock;
public function setBlock(BlockInterface $block)
{
if($block instanceof TextBlock) {
$this->textBlock = $block;
} elseif($block instanceof PictureBlock) {
$this->pictureBlock = $block;
}
}
}
This is an anti-pattern, because it violates the open close principle ("Open for extensions and close for modifications"). But we need to edit this file every time we want to add a block.
So let's have a look at the other possible implementation.
+----+--------------+----------+
| id | block_class | block_id |
+----+--------------+----------+
| 1 | TextBlock | 1 |
+----+--------------+----------+
| 2 | PictureBlock | 1 |
+----+--------------+----------+
With this structure we don't need any further columns for blocks we add. We just need to inform someone that a TextBlock will resolve to an sql look up on the TextBlock table, which is a easy feature in doctrine.
On the other hand, we lose some sql features like foreign keys and thus delete constraints. This is a risk for inconsistent data. In some cases we can help ourselves by creating a reference from the inverse block tables back to the page table.
And how does our php code look like? Well, it's strait forward. We just implement the common polymorphism.
class Page
{
private $block;
private $blockClass;
private $blockId;
public function setBlock(BlockInterface $block)
{
$this->block = $block;
}
}
Configuration
To make it work, the EnhavoDoctrineExtensionBundle
will hooks into doctrine and takes care of resolving $blockClass
and $blockId
, and injects the correct block if you got the object from a repository.
For that you have to add some ORM mapping to your entity like in this example.
namespace App\Entity
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Page
{
/** @var BlockInterface */
private $block;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $blockClass;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $blockId;
public function setBlock(BlockInterface $block)
{
$this->block = $block;
}
}
Now tell the EnhavoDoctrineExtensionBundle
for which class you want to add the reference behavior and which field the listener has to observe.
enhavo_doctrine_extension:
metadata:
App/Entity/Page:
reference:
block:
idField: blockId
nameField: blockClass
That's all. The EnhavoDoctrineExtensionBundle
takes care of App/Entity/Page