Auto-configuration of Doctrine repositories as services
Published on 2020-02-19 • Modified on 2020-03-14
In this post, we will see how to use Doctrine repositories as services without adding additional configuration to the services.yaml file. Let's go! 😎
» Published in "A week of Symfony 686" (7-23 February 2020) and Jetbrains PHP Annotated – March 2020.
Prerequisite
I will assume you know what a Doctrine repository is and how to use it in a Symfony application to retrieve data from a database.
Configuration
- PHP 8.3
- Symfony 6.4.12 with autowiring activated.
Introduction
Sometimes, when developing, when you know how to do something, you just reproduce what you did in the past and you don't ask yourself if there is a better way to do it (baaaad!). I recently migrated this blog to Symfony 5 (not without pain). Then I checked my doctrine.yaml
file, its content was: (I have commented the lines because they are not used anymore, I left them for this blog post):
# config/packages/doctrine.yaml
#services:
# App\Entity\ArticleRepository:
# factory: ['@doctrine.orm.default_entity_manager', getRepository]
# arguments:
# - App\Entity\Article
I said to myself: I have Symfony 5, Flex, and autowiring. Why should I have to declare Doctrine repositories manually? The answer is: we don't have to. Then, I remember I read this excellent blog post of Tomas Vortuba a while ago on this subject. It's two years old, but it's worth reading it (with the comments). He shows different approaches with their associated pros and cons. In the comments, Melyou pointed out the existence of the Doctrine ServiceEntityRepository
class and someone added explanations about it. So I had to give it a try.
The goal is to delete this configuration. So, if you still have this kind of setup, let's see how to do this. If you want to create a new entity, run the maker:entity
command and you're done (you must have the MakerBundle installed).
Moving repositories from the entity folder
In the past, we used to put the Doctrine repositories in the same directory than the entities. And they aren't autoloaded because of this default autowiring configuration in the config/services.yaml
file where the Entity
folder is excluded:
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
So, the first step is to move them in a new directory, let's use src/Repository
(the maker command also uses this):
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Article;
use App\Enum\ArticleType;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @method Article|null findOneById(int $id)
* @method Article|null findOneBySlug(string $slug)
*
* @extends ServiceEntityRepository<Article>
*/
final class ArticleRepository extends ServiceEntityRepository
{
use BaseRepositoryTrait;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
The namespace is now in App\Repository
instead of App\Entity
. The main difference with our old file is that it now extends the ServiceEntityRepository
class. To make things work, we must add a constructor where we configure the entity linked to this repository. It's the equivalent of the factory call we had before in our doctrine configuration file. But it's cleaner here as everything is configured in the same file. The rest of the code remains the same.
Click here to see the Doctrine ServiceEntityRepository
class code.
<?php
namespace Doctrine\Bundle\DoctrineBundle\Repository;
use Doctrine\ORM\EntityRepository;
use function property_exists;
if (property_exists(EntityRepository::class, '_entityName')) {
// ORM 2
/**
* Optional EntityRepository base class with a simplified constructor (for autowiring).
*
* To use in your class, inject the "registry" service and call
* the parent constructor. For example:
*
* class YourEntityRepository extends ServiceEntityRepository
* {
* public function __construct(ManagerRegistry $registry)
* {
* parent::__construct($registry, YourEntity::class);
* }
* }
*
* @template T of object
* @template-extends LazyServiceEntityRepository<T>
*/
class ServiceEntityRepository extends LazyServiceEntityRepository
{
}
} else {
// ORM 3
/**
* Optional EntityRepository base class with a simplified constructor (for autowiring).
*
* To use in your class, inject the "registry" service and call
* the parent constructor. For example:
*
* class YourEntityRepository extends ServiceEntityRepository
* {
* public function __construct(ManagerRegistry $registry)
* {
* parent::__construct($registry, YourEntity::class);
* }
* }
*
* @template T of object
* @template-extends ServiceEntityRepositoryProxy<T>
*/
class ServiceEntityRepository extends ServiceEntityRepositoryProxy
{
}
}
Adding generic methods to your repositories
As the repository now extends the ServiceEntityRepository
class, I use a BaseRepositoryTrait
to provide some generics functions, let's have a look at it:
<?php
declare(strict_types=1);
namespace App\Repository;
use Doctrine\ORM\EntityManagerInterface;
/**
* @method EntityManagerInterface getEntityManager()
*/
trait BaseRepositoryTrait
{
/**
* This is just an example. Use ->count([]) for the same result.
*/
public function countAll(): int
{
/** @var string $count */
$count = $this->getEntityManager()
->createQuery(\sprintf('SELECT COUNT(a) FROM %s a', $this->getClassName()))
->getSingleScalarResult();
return (int) $count;
}
To have autocompletion, the trick here is to add @method
or @property
annotations so your IDE knows the type of the objects you are dealing with. In this case, the getEntityManager()
function returns an EntityManagerInterface
. It's something that is also used in the Symfony code base, here is an example (check out the last line in the following snippet):
Click here to see the Symfony CompiledUrlMatcherTrait.php
code.
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*
* @property RequestContext $context
*/
trait CompiledUrlMatcherTrait
{
private bool $matchHost = false;
private array $staticRoutes = [];
private array $regexpList = [];
private array $dynamicRoutes = [];
private ?\Closure $checkCondition;
public function match(string $pathinfo): array
{
$allow = $allowSchemes = [];
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!$this instanceof RedirectableUrlMatcherInterface) {
throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {
Using the repository as a service
Well, nothing special here. We are done. We can use our repository like any other services. For example, let's have a look at the controller responsible for displaying the homepage of this website:
{
public function __construct(
public readonly ArticleRepository $articleRepository,
) {
We inject the service in the method parameters with the correct class type hint. Et voilà! Now, every time you will add a new repository like this it will be available in the same way without having to modify the configuration.
Conclusion
I've just made this change in this project. So, let's see how it goes. But for me, it seems cleaner with what I used to do. Adding the entry in the service file was a pain with no advantage at all. The fact that this approach is used by the MakerBundle shows that it is considered a best practice.
That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊
Read the doc More on the web More on Stackoverflow
They gave feedback and helped me to fix errors and typos in this article; many thanks to jmsche, TimoBakx, gubler. 👍
Call to action
Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )
- Report any error/typo.
- Report something that could be improved.
- Like and retweet!
- Follow me on Twitter Follow me on Twitter
- Subscribe to the RSS feed.
- Click on the More on Stackoverflow buttons to make me win "Announcer" badges 🏅.
Thank you for reading! And see you soon on Strangebuzz! 😉
[🇬🇧] Auto-configuration of #doctrine repositories as services. Challenging yourself to improve your code. 💪 https://t.co/wCXIm5piKd Proofreading, comments, likes and retweets are welcome! 😉 Annual goal: 1/6 (16%) #php #strangebuzz #blog #blogging #bestpractices /cc @VotrubaT
— COil #onEstLaTech ✊ (@C0il) February 20, 2020