Custom Doctrine Repository Factory example

Hey,

as there was a question about how to implement an own repository factory within laminas / mezzio to realize custom dependencies in a constructor of a doctrine entity repository, which was deleted by the author, I wrote this small example.

Doctrine uses it 's own repository factory, which you can override in your doctrine config with your own respository factory. Having a look into the original doctrine repository factory you only have to implement the RepositoryFactory interface.

Your custom factory

You just write your own repository factory, which you 'll use for your repositories with dependencies in the future.

<?php

declare(strict_types=1);

namespace Marcel\ORM\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Repository\RepositoryFactory;
use Psr\Container\ContainerInterface;

class MyCustomRepositoryFactory implements RepositoryFactory
{
    protected array $repositoryList = [];

    public function __construct(
        protected ContainerInterface $container,
    ) {}

    public function getRepository(
        EntityManagerInterface $entityManager, 
        string $entityName
    ): EntityRepository
    {
        $repositoryHash = $entityManager
            ->getClassMetadata($entityName)
            ->getName() . spl_object_hash($entityManager);

        if (isset($this->repositoryList[$repositoryHash])) {
            return $this->repositoryList[$repositoryHash];
        }

        return $this->repositoryList[$repositoryHash] = 
            $this->createRepository($entityManager, $entityName);
    }

    protected function createRepository(
        EntityManagerInterface $entityManager, 
        string $entityName
    ): EntityRepository 
    {
        $metadata = $entityManager->getClassMetadata($entityName);
        $repositoryClassName = $metadata->customRepositoryClassName
            ?: $entityManager
                ->getConfiguration()
                ->getDefaultRepositoryClassName();
        
        // here 's the magic
        // the service manager recognizes the fqcn of the repository and checks if a
        // factory was configured for this repository
        return ($this->container->has($repositoryClassName) 
            ? $this->container->get($repositoryClassName);
            : new $repositoryClassName($entityManager, $metadata);
    }
}

Factory for your repository factory

Since your custom factory has a dependency to the service container you have to write a factory for the factory. After all these years using Zend / Laminas I still feel a bit uncomfy with factories for factories. But that 's the way it works I guess. If anyone has a better approach I 'd be curious to read about it.

<?php

declare(strict_types=1);

namespace Marcel\ORM\Repository\Factory;

use Marcel\ORM\Repository\MyCustomRepositoryFactory;
use Psr\Container\ContainerInterface;

class MyCustomRepositoryFactoryFactory
{
    public function __invoke(
        ContainerInterface $container
    ): MyCustomRepositoryFactory
    {
         $factory = new MyCustomRepositoryFactory($container);
         return $factory;
    }
}

Service manager config

As always factories have to be registered in the service manager via config.

...
'service_manager' => [
    ORM\Repository\MyCustomRepositoryFactory::class 
        => ORM\Repository\Factory\MyCustomRepositoryFactoryFactory::class,
],
...

Doctrine config

Finally edit your doctrine config

    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'repository_factory' => ORM\Repository\MyCustomRepositoryFactory::class,
            ],
        ],
    ]

Code is not tested.

Hi @ezkimo, can you share an example of why one would want to override the doctrine repository? Thanks!

Hi @ezkimo ,
since I was the one who created and deleted the question, let me say thanks for answering the question anyway. Minutes after posting the question, I’ve implemented a solution that helped me create my repository in Mezzio context. Actually I had to way of injecting dependencies into a Doctrine Repository:

  • Dependency injection via constructor - similar to your solution
  • Dependency injection via setter … over a delegator

I’ll come back later and post my solution.

Note: Does it make sense to re-open my question?

1 Like

As I’ve said earlier I’m back with the solution I took to inject
dependencies into a doctrine repository class (in a Mezzio application).

I have for example an entity class:

<?php

declare(strict_types=1);

namespace App\Entity

use Doctrine\ORM\Mapping as ORM;
use App\Repository\PersonRepository;

#[ORM\Entity(repositoryClass: PersonRepository::class)]
#[ORM\Table(name: 'persons')]
#[ORM\Index(name: 'first_name_idx', columns: ['first_name'])]
#[ORM\Index(name: 'last_name_idx', columns: ['last_name'])]
class Person
{
    #[ORM\Id()]
    #[ORM\Column(name: 'id', type: 'bigint', nullable: false)]
    #[ORM\GeneratedValue(strategy: 'IDENTITY')]
    private int $id;

    #[ORM\Column(name: 'first_name', type: 'string', length: 255, nullable: false,)]
    private string $firstName;

    #[ORM\Column(name: 'last_name', type: 'string', length: 255, nullable: false,)]
    private string $lastName;
}

And the corresponding custom entity repository where, I need to inject an instance of the BarInterface::class

<?php

declare(strict_types=1);

namespace App\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use App\Entity\Person;
use App\Foo\BarInterface;

class PersonRepository extends EntityRepository
{
    public function __construct(
        private readonly BarInterface $bar,
        private readonly EntityManagerInterface $em,
    )
    {
        parent::__construct(em: $em, class: new ClassMetadata(Person::class));
    }

    public function specializedDQL(): QueryBuilder
    {
        return $this->createQueryBuilder(alias: 'p')
            ->where('p.firstName = :firstName')
            ->setMaxResults(maxResults: 50)
            ->setParameter('firstName', 'John');
    }
}

With this setup, fetching the repository via the EntityManager::getRepository() method would fail, since
it is relying on the DefaultRepositoryFactory::getRepository() to create the PersonRepository::class.

So I did the following:

Create a factory for the PersonRepository::class:

<?php

declare(strict_types=1);

namespace App\Repository\Factory;

use Doctrine\ORM\EntityManager;
use App\Repository\PersonRepository;
use Psr\Container\ContainerInterface;
use App\Foo\BarInterface;

class PersonRepositoryFactory
{
    public function __invoke(ContainerInterface $container): PersonRepository
    {
        return new PersonRepository(
            bar: $container->get(BarInterface::class),
            em: $container->get(EntityManager::class),
        );
    }

}

Register the factory:

<?php

declare(strict_types=1);

namespace App;

use App\Repository\Factory\PersonRepositoryFactory;
use App\Repository\PersonRepository;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies' => $this->getDependencies(),
        ];
    }

    public function getDependencies(): array
    {
        return [
            'invokables' => [
            ],
            'factories' => [
                PersonRepository::class => PersonRepositoryFactory::class,
            ],
        ];
    }
}

The custom doctrine entity repository, can now be fetched from the ContainerInterface, and it will have the “hard”
dependency BarInterface::class injected.

@ALTAMASH80
The reason for wanting to override doctrine repositories, could more complex queries are needed.

From doctrine documentation:

… .Compared to DQL these query methods are falling short of functionality very fast.
Doctrine offers you a convenient way to extend the functionalities of the default EntityRepository and put all the
specialized DQL query logic on it. For this you have to create a subclass of Doctrine\ORM\EntityRepository. …

I thought you were going to show me the way to Venus. Instead, you’ve shown me a California dry hills and said Venus looks like this. I’m impressed by your solution. But, one other solution still missing can you share that as well? The delegator pattern? Thanks!

Hi,

My intention wasn’t to impress but to share. I encountered issues when trying to fetch a repository from
the EntityManager::class; a classic example of not paying attention. The solution described by @ezkimo helps to do it that way.

I also believe that using a delegator isn’t special. I had a dependency that I wanted to inject and
a corresponding setter method for the class attribute, something like this:

<?php

declare(strict_types=1);

namespace App\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use App\Foo\BarInterface;

class PersonRepository extends EntityRepository
{
    private BarInterface $bar;
    
    /**
     * @psalm-return list<Person>
     */
    public function specializedDQL(): array
    {
        return [
            // some data
        ];
    }
    
    public function setBar(BarInterface $bar) {
        $this->bar = $bar;
    }
    
}

and the delegator

<?php

declare(strict_types=1);

namespace Erp;

use Erp\Repository\PersonRepository;use Mezzio\Application;
use Psr\Container\ContainerInterface;

class PersonRepositoryDelegatorFactory
{
    public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): PersonRepository
    {       
        /** @var PersonRepository $personRepository */
        $personRepository = $container->get(PersonRepository::class);
        
        return $personRepository->setBar($container->get(BarInterface::class));
    }

}

I think this implementation of the delegator would also work, with the given setup

<?php

declare(strict_types=1);

namespace Erp;

use Erp\Repository\PersonRepository;use Mezzio\Application;
use Psr\Container\ContainerInterface;

class PersonRepositoryDelegatorFactory
{
    public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): PersonRepository
    {
        /** @var PersonRepository $personRepository */
        $personRepository = $callback();
        
        return $personRepository->setBar($container->get(BarInterface::class));
    }

}

Register the delegator

<?php

declare(strict_types=1);

namespace App;

use App\Repository\Factory\PersonRepositoryFactory;
use App\Repository\PersonRepository;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies' => $this->getDependencies(),
        ];
    }

    public function getDependencies(): array
    {
        return [
            'delegators' => [
                PersonRepository::class => [
                    PersonRepositoryDelegatorFactory::class,
                ],
            ],
            'invokables' => [
            ],
            'factories' => [
                // Some factories...
            ],
        ];
    }
}

1 Like

Hey @ALTAMASH80 ,

as @Serge_Nguimjeu already stated out some use cases, it 's generally all about dependencies. If you have a constructor dependency or a setter dependency the service manager is the way to go. The custom doctrine repository factory gives you an easy way to register your entity repositories via config for the laminas service manager.

Registering the custom doctrine repository factory as shown above, you can easily gain your entity repositories from the service manager as in the following example:

<?php

declare(strict_types=1);

namespace Marcel\ORM\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Marcel\Service\BlaDependency;
use Marcel\Service\YaddaDependency;

class MyEntityRepository extends EntityRepository
{
    public function __construct(
        protected YaddaDependency $yaddaDependency,
        protected BlaDependency $blaDependency,
        EntityManagerInterface $entityManager,
        ClassMetadata $metadata
    ) {
        parent::__construct($entityManager, $metadata);
    }

    ...
}

As you can see the MyEntityRepository class has two more dependencies in the constructor, as the default doctrine entity repository has. These two dependencies are resolved over a specific factory.

<?php

declare(strict_types=1);

namespace Marcel\ORM\Repository\Factory;

use Doctrine\ORM\EntityManager;
use Marcel\ORM\Entity\MyCustomEntity;
use Marcel\ORM\Repository\MyCustomRepository;
use Marcel\Service\BlaDependency;
use Marcel\Service\YaddaDependency;
use Psr\Container\ContainerInterface;

class MyCustomRepositoryFactory
{
    public function __invoke(ContainerInterface $container): MyCustomRepository
    {
        $yaddaDependency = $container->get(YaddaDependency::class);
        $blaDependency = $container->get(BlaDependency::class);

        $entityManager = $container->get(EntityManager::class);
        $metadata = $entityManager->getClassMetadata(MyCustomEntity::class);

        return new MyCustomRepository(
            $yaddaDependency,
            $blaDependency,
            $entityManager,
            $metadata,
        );
    }
}

Just register it in the service manager config as follows …

...
'service_manager' => [
    'factories' => [
        ORM\Repository\MyCustomRepository::class 
            => ORM\Repository\Factory\MyCustomRepositoryFactory::class,
    ],
],
...

That 's how it would work in an MVC application. The examples Serge gave for Mezzio dependencies are also very good.

Hi @ezkimo, I’ve no issues with what @Serge_Nguimjeu shared. I presumed things in a different context. That is why I was disappointed. I’ve no issue with the solution shared. I hope it helps someone out. Thanks!

1 Like