Doctrine+Symfony: adding indexes to fields defined in traits

Unrelated but beautiful photo by Joanna Malinowska

The task: make a trait with some columns that you can reuse in entity classes. Basically it just works:

<?php
namespace App\Entity\Utils;

use Doctrine\ORM\Mapping as ORM;

And later in the entity class:

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

But what if you need to have an index on your reused column? You can’t add it to trait as Doctrine wants to see @ORM\Table only on real tables. And you don’t want to add it on every entity where you use that trait — that’s the opposite of code reuse!

What you need to do is an event listener that will add index to all relevant entities automatically:

<?php
namespace App\Utils;

use App\Entity\Utils\MyTrait;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;

class MyEntityListener implements EventSubscriber
{
public function getSubscribedEvents()
{
return [
'loadClassMetadata',
];
}

public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$cm = $eventArgs->getClassMetadata();
$class = $cm->getName();
$uses = class_uses($class);

if (in_array(MyTrait::class, $uses)) {
$cm->table['indexes'][] = [
'columns' => [
'some_field',
],
];
}
}
}

And don’t forget to register the listener in services.yaml:

services:
App\Utils\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: loadClassMetadata }

Note missing key above in $cm->table['indexes'][]: you technically can specify index name there, but this means all entities with that trait will have same index name. This is not a problem for MySQL (I guess index name is local to owning table), but in SQLite you’ll get a duplicate index error. So, by omitting index name you tell Doctrine to generate unique name every time it is used.

Similar technique works for inherited tables with @ORM\MappedSuperclass, just replace class_uses/in_array with instance of.