Doctrine+Symfony: adding indexes to fields defined in traits

Alex Kunin
Jul 11 · 2 min read
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;
trait MyTrait
{
/**
* @var string
* @ORM\Column("some_field", type="string")
*/
protected $someField;

public function setSomeField(string $someField): self
{
$this->someField = $someField;
return $this;
}

public function getSomeField(): string
{
return $this->someField;
}
}

And later in the entity class:

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class MyEntity
{
use App\Entity\Utils\MyTrait;
}

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade