Doctrine 2 Index Inheritance

Viewed 1300

i am new to Doctrine 2 am i trying to figure out how to do index inheritance with it.

What i am trying to achieve is to have a base class that defines some default columns along with necessary indexes for all entities within the application.

Example: All tables in my application will have created_on and modified_on, so i prepared one base @MappedSuperclass with those 2 columns in it.

Here is my code:

<?php

/**
 * @MappedSuperclass
 */
abstract class EntityAbstract
{
    /**
     * @Column(type="datetime", name="created_on", nullable=false)
     */
    protected $createdOn;

    /**
     * @Column(type="datetime", name="modified_on", nullable=true)
     */
    protected $modifiedOn;

}

/**
 * @Entity
 * @Table(name="member", indexes={@Index(name="credential", columns={"email_address", "password"})})
 */
class Member extends EntityAbstract
{
    /**
     * @Column(type="string", name="full_name", length=50)
     */
    protected $fullName;

    /**
     * @Column(type="string", name="email_address", length=50, unique=true, nullable=false)
     */
    protected $emailAddress;

    /**
     * @Column(type="string", name="password", length=40, nullable=false)
     */
    protected $password;       

}
?>

I want to enforce created_on to be an index, so i put @Index annotation in the base class for this particular column. Hoping that this will result in 2 indexes for Member which is created_on and email_address+password combination. This however results in indexes of the base class being overriden by the child class, thus created_on is not an index.

/**
 * @MappedSuperclass
 * @Table(indexes={@Index(name="timestampcreated", columns={"created_on"})})
 */
abstract class EntityAbstract

How do i achieve this in Doctrine 2? Have looked at single table inheritance, but my understanding is that it's meant for different purpose.

3 Answers

Here's a full solution for the way laid out at https://github.com/doctrine/orm/issues/5928#issuecomment-273624392, thanks to https://medium.com/@alexkunin/doctrine-symfony-adding-indexes-to-fields-defined-in-traits-a8e480af66b2

namespace App\EventListener;

use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use App\Entity\Member;

class MemberIndexListener
{
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var Doctrine\ORM\Mapping\ClassMetadata $classMetadata */
        $classMetadata = $eventArgs->getClassMetadata();
        if (Member::class === $classMetadata->getName())
        {
            $prefix = strtolower((new \ReflectionClass($classMetadata->getName()))->getShortName()); // you can omit this line and ...
            $classMetadata->table['indexes'][$prefix.'_created_on'] =  // ... replace `[$prefix.'_created_on']` with `[]` for automatic index naming
                ['columns' => ['username']
            ];
            // For UniqueConstraints, use:
            // $classMetadata->table['uniqueConstraints'][...] = ...
        }
    }
}

And in services.yaml:

services:
    App\EventListener\MemberIndexListener:
        tags:
            - { name: doctrine.event_listener, event: loadClassMetadata }

This is my solution for automatic index inheritance.

You can specify indexes in table annotation of mapped superclass. And all children will extend them.

The listener takes indexes of all parent mapped superclases.

#src/EventListener/InheritIndexListener.php
<?php

namespace App\EventListener;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;

final class InheritIndexListener
{
    /** @var EntityManagerInterface */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
    {
        $classMetadata = $eventArgs->getClassMetadata();
        $name = $classMetadata->name;

        if (!$classMetadata->isMappedSuperclass) {
            foreach (class_parents($name) as $parent) {
                $this->addParentIndexes($parent, $classMetadata);
            }
        }
    }

    private function addParentIndexes(string $parent, ClassMetadata $classMetadata): void
    {
        $parentMetadata = $this->entityManager
            ->getClassMetadata($parent);

        foreach ($parentMetadata->table['indexes'] as $index) {
            if (!$this->hasSameIndex($index, $classMetadata->table['indexes'])) {
                $classMetadata->table['indexes'][] = $index;
            }
        }

        foreach ($parentMetadata->table['uniqueConstraints'] as $uniqueConstraint) {
            if (!$this->hasSameIndex($uniqueConstraint, $classMetadata->table['uniqueConstraints'])) {
                $classMetadata->table['uniqueConstraints'][] = $uniqueConstraint;
            }
        }
    }

    private function hasSameIndex(array $needle, array $haystack): bool
    {
        foreach ($haystack as $item) {
            if ($item['columns'] === $needle['columns']) {
                return true;
            }
        }

        return false;
    }
}


#config/services.yaml
services:
    App\EventListener\InheritIndexListener:
        tags:
            - { name: doctrine.event_listener, event: loadClassMetadata }

And entities.

#src/Entity/BaseResource.php
<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\MappedSuperclass
 * @ORM\Table(
 *     indexes={
 *     @ORM\Index(columns={"external_id"}),
 *     }
 *     )
 */
abstract class BaseResource
{
    /**
     * @var int|null
     * @ORM\Column(type="integer")
     */
    private $externalId;
    //...
}

#src/Entity/BaseSlugableResource.php
<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\MappedSuperclass
 * @ORM\Table
 *     uniqueConstraints={
 *     @ORM\UniqueConstraint(columns={"slug"}),
 *     },
 *     )
 */
abstract class BaseSlugableResource extends BaseResource
{
    /**
     * @var string|null
     * @ORM\Column(type="string")
     */
    private $slug;
    //...
}

#src/Entity/Article.php
<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Article extends BaseSlugableResource
{
    //...
}

In result the Article entity will have externalId index and slug unique index.

Related