How to use Repository with Doctrine as Service in Symfony

Dependency injection with autowiring is super easy since Symfony 3.3. Yet on my mentoring I still meet service locators. 

Mostly due to traditional registration of Doctrine repositories. 

The way out from service locators to repository as service was described by many before and now we put it into Symfony 3.3 context.

Are you too lazy to refactor your code this way manually?

No worries, me too. So let's have a coffee and let Rector handle this for you!

 

This post is follow up to StackOverflow answer to clarify key points and show the sweetest version yet.

The person who kicked me to do this post was Václav Keberdle - thank you for that.

Clean, Reusable, Independent and SOLID Goal

Our goal is to have clean code using constructor injectioncomposition over inheritance and dependency inversion principles.

With as simple registration as:

# app/config/services.yml

services:
    _defaults:
        autowire: true

    App\Repository\:
        resource: ../Repository

IDE plugins an other workarounds put aside, because this code can be written just with typehints.

How do we Register Repositories Now

1. Entity Repository

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
    /**
     * Our custom method
     *
     * @return Post[]
     */
    public function findPostsByAuthor(int $authorId): array
    {
        return $this->findBy([
            'author' => $authorId
        ]);
    }
}

 Advantages

It's easy and everybody does that.

You can use prepared methods like findBy()findOneBy() right away.

 Disadvantages

If we try to register repository as a service, we get this error:

Why? Because parent constructor of Doctrine\ORM\EntityRepository is missing EntityManagertypehint

Also we can't get another dependency, like PostSorter that would manage sorting post in any way.

<?php declare(strict_types=1);

namespace App\Repository;

use App\Sorter\PostSorter;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
    public function __construct(PostSorter $postSorter)
    {
        $this->postSorter = $postSorter;
    }
}

Because parent constructor requires EntityManager and ClassMetadata instances.

Those prepared methods like findBy() don't have argument nor return typehints, so this would pass:

$this->postRepository->find('someString');

And we don't know what object we get back:

$post = $this->postRepository->find(1);
$post->whatMethods()!

 

2. Entity

<?php declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Entity;
use Doctrine\ORM\EntityRepository;

/**
 * @Entity(repositoryClass="App\Repository\PostRepository")
 */
final class Post
{
    ...
}

This reminds me of circular dependency and active record pattern from Doctrine 4. Why should entity know about its repository?

Do you know why we need repositoryClass="PostRepository"?

It's form of service locator, that basically works like this:

$this->entityManager->getRepository(Post::class);

Instead of registration to Symfony container like any other service, here is uses logic coupled to annotation of specific class. Just a reminder: Occam's razor.

 Advantages

It's in documentation.

 Disadvantages

It is very complicated to have more repositories for one entity. What if I want to have PostRedisRepository for Redis-related operations and PostFrontRepository for reading-only?

We're losing all features of our framework's Dependency Injection container (events, collections, autowiring, automated registration, logging etc.).

 

3. Use in Controller

You have to use this complicated service registration in YAML:

services:
    app.post_repository:
        class: Doctrine\ORM\EntityRepository
        factory: ['@doctrine.orm.default_entity_manager', getRepository]
        arguments:
            - App\Entity\Post

...or just pass EntityManager.

<?php declare(strict_types=1);

namespace App\Controller;

use App\Entity\Post;
use App\Repository\PostRepository;
use Doctrine\ORM\EntityManager;

final class PostController
{
    /**
     * @var PostRepository
     */
    private $postRepository;

    public function __construct(EntityManager $entityManager)
    {
        $this->postRepository = $entityManager->getRepository(Post::class);
    }
}

 Advantages

Again, status quo.

 Disadvantages

IDE doesn't know it's App\Repository\PostRepository, so we have add extra typehint (so boringwork). Example above would work because there is typehinted property , but this would fail:

$postRepository = $entityManager->getRepository(Post::class);
$postRepository->help()?;

Or this:

$post = $this->postRepository->find(1);
$post->help()?;

To enable autocomplete, we have to add them manually:

/** @var App\Entity\Post $post */
$post = $this->postRepository->find(1);
$post->getName();

This annotation helper should never be in your code, except this case:

/** @var SomeService $someService */
$someService = $container->get(SomeService::class);

 

4. Registration services.yml

None. Repositories are created by Doctrine.

 

 Advantages Summary

It's easy to copy-paste if already present in our code.

It's spread in most of documentation, both in Doctrine and Symfony and in many posts about Doctrine.

No brain, no gain.

 Disadvantages Summary

We cannot use autowiring.

We cannot inject repository to other service just via constructor.

We have to typehint manually everything (IDE Plugins put aside).

We have Doctrine in our Controller - Controller should only delegate to model, without knowing what Database package is used.

To allow constructor injection, we have to prepare for much config programming.

Thus it's coupled to the framework you use and less reusable.

We cannot use multiple repository for single entity. It naturally leads to huge repositories.

We cannot use constructor injection in repositories, which can easily lead you to creating static helper classes.

Also, you directly depend on Doctrine's API, so if find() changes to get() in one composer update, your app is down.

How to Make This Better with Symfony 3.3?

It require few steps, but all builds on single one change. Have you heard about composition over inheritance?

Instead of inheritance...

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
}

...we use composition:

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;

final class PostRepository
{
    /**
     * @var EntityRepository
     */
    private $repository;

    public function __construct(EntityManager $entityManager)
    {
        $this->repository = $entityManager->getRepository(Post::class);
    }
}

That's all! Now you can program the way which is used in the rest of your application:

  • class,
  • service
  • and constructor injection

And how it influenced our 4 steps?

 

1. Entity Repository

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use App\Sorter\PostSorter;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;

final class PostRepository
{
    /**
     * @var EntityRepository
     */
    private $repository;

    /**
     * @var PostSorter
     */
    private $postSorter;

    public function __construct(EntityManager $entityManager, PostSorter $postSorter)
    {
        $this->repository = $entityManager->getRepository(Post::class);
        $this->postSorter = $postSorter;
    }

    public function find(int $id): ?Post
    {
        return $this->repository->find($id);
    }
}

 Advantages

Everything is strictly typehintedno more frustration from missing autocompletion.

Constructor injection works like you expect it to.

You can get another dependency if you like.

 

2. Entity

<?php declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Entity;
use Doctrine\ORM\EntityRepository;

class Post
{
    ...
}

 

 Advantages

Clean and standalone object.

No service locators smells.

Allows multiple repositories per entity:

- App\Repository\ProductRepository
- App\Repository\ProductRedisRepository
- App\Repository\ProductBenchmarkRepository

 

3. Use in Controller

<?php declare(strict_types=1);

namespace App\Controller;

use App\Repository\PostRepository;

final class PostController
{
    /**
     * @var PostRepository
     */
    private $postRepository;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
    }
}

 Advantages

IDE knows the type and autocomplete 100% works.

There is no sign of Doctrine.

Easier to maintain and extend.

Also space to decoupling to local packages is now opened.

 

4. Registration services.yml

Final 3rd appearance for it's great success:

# app/config/services.yml

services:
    _defaults:
        autowire: true

    App\Repository\:
        resource: ../Repository

 

All we needed is to apply composition over inheritance pattern in this specific case.

If you don't use Doctrine or you already do this approach, try to think where else you extends 3rd party package instead of __construct.

How to add new repository?

The main goal of all this was to make work with repositories typehinted, safe and reliable for you tu use and easy to extends.

It also minimized space for error, because strict types and constructor injection now validatesmuch of your code for you.

The answer is now simple: just create repository it in App\Repository.

Try the same example with your current approach and let me know in the comments.

Happy coding!

https://www.tomasvotruba.cz/blog/2017/10/16/how-to-use-repository-with-doctrine-as-service-in-symfony/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值