61

When I read docs about repositories, it is often to work with entities & collection but in a "read-only" manner.

There are never examples where repositories have methods like insertUser(User $user) or updateUser(User $user).

However, when using SOA, Service should not be working with Entity Manager (that's right, isn't it?) so:

  1. Should my service be aware of the global EntityManager?
  2. Should my service know only about the used Repositories (let's say, UserRepository & ArticleRepository)

From that both questions, another one, should my service ever explicitly persist() & flush() my entities ?

Trent
  • 5,785
  • 6
  • 32
  • 43

3 Answers3

48

Yes, repositories are generally used for queries only.

Here is how I do it. The service layer manages the persistence. The controller layer knows of the service layer, but knows nothing of how the model objects are persisted nor where do they come from. For what the controller layer cares is asking the service layer to persist and return objects — it doesn't care how it's actually done.

The service layer itself is perfectly suitable to know about the the persistence layer: entity or document managers, repositories, etc.

Here's some code to make it clearer:

class UserController
{
    public function indexAction()
    {
        $users = $this->get('user.service')->findAll();
        // ...
    }

    public function createAction()
    {
        // ...
        $user = new User();
        // fill the user object here
        $this->get('user.service')->create($user);
        // ...
    }
}

class UserService
{
    const ENTITY_NAME = 'UserBundle:User';

    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function findAll()
    {
        return $this->em->getRepository(self::ENTITY_NAME)->findAll();
    }

    public function create(User $user)
    {
        // possibly validation here

        $this->em->persist($user);
        $this->em->flush($user);
    }
}
Elnur Abdurrakhimov
  • 44,533
  • 10
  • 148
  • 133
  • I'm curious how you implement this (persisting from the service layer.) Usually I create and persist entities from the controller, but admit that it doesn't feel 'right'. Do you use 1 service per entity that needs persisting? Would you mind going into more detail? – Steven Mercatante Dec 03 '11 at 19:56
  • 14
    Persisting in the controller layer doesn't feel right because controllers should bother with GUI only, i.e. they just parse the requests, delegate all the “real” work to the service layer, and return responses. Most of the time I have a service per entity/document, but that's not a rule, because sometimes you can have services without entities at all and theoretically (I wasn't there yet) services that manage several entities. – Elnur Abdurrakhimov Dec 03 '11 at 20:13
  • 1
    “GUI” was the wrong term, because not all interfaces are graphical. There can be REST, command line and other kinds of interfaces, which all delegate the “real” work to the service layer. Using the service layer helps to adhere to the DRY principle, since you don't have the same code in different types of interfaces, and decouples them from the persistence layer, since one day you might want to switch from ORM to ODM, for example, or from local storage to some 3rd party storage like clouds. – Elnur Abdurrakhimov Dec 03 '11 at 20:25
  • Interesting. I agree that using the service layer for persistence is more DRY than using the controller layer. In your example you're not setting any of the User properties, would you set those in the controller, or pass them to the service's `create` method? Something that bugs me however is `findAll` being proxied by the service. I don't see an advantage in using the service over the repository itself. – Steven Mercatante Dec 03 '11 at 20:36
  • Good point. I've updated the code to demonstrate where the user properties are being set. They come from interfaces: forms, REST requests, etc. I pass the user object to the service layer — the object has all the information that's needed to save it properly. Regarding the `find` queries, the service layer acts as a layer of [facades](http://en.wikipedia.org/wiki/Facade_pattern), to simplify and decouple the rest of the system. If you decide to switch from ORM to ODM or some sort of cloud, repositories might stop making any sense and you'll have much more code to rewrite. – Elnur Abdurrakhimov Dec 03 '11 at 21:19
  • @elnur And if I use a factory to retrieve Repository classes and use that in the Controller, but continue to use the Service Layer? (I will use the factory only to load Users from the Repository), if I switch to the ODM, I could just change the factory. Actually I use something like this inside controllers: `$user = $this->_helper->repository( 'User' )->find( $id )`. – jonathancardoso Jan 27 '12 at 23:58
  • Another reason is that persisting/flushing in the service layer helps with transactions and rollbacks. If you persisted & flushed in each repository, then you will have to deal with rolling back manually if subsequent queries you need fails. If you did the save in the service layer, you can just wrap all operations and rollback if any fails. – James Cowhen Jun 03 '13 at 19:24
  • 1
    Repositories (as in the [Repository Pattern](http://code.tutsplus.com/tutorials/the-repository-design-pattern--net-35804)) absolutely can have methods for "adding" or "saving". – John Pancoast Jan 26 '15 at 23:01
  • This is the exact approach I take when developing my applications. Regarding input to be passed to the service layer, I handle that in my controllers. My service layer accepts objects only. i.e. if my controller is accepting a submit adding a new user, a User object is created and populated, then passed to the service layer. Regarding the findAll(), keeping all business logic grouped into a single service is worth it. Fellow devs don't need search, they have their tool set in one place. – Layton Everson Oct 21 '15 at 04:41
  • why you didnt do fill the user object in create method of service ? ! – babak faghihian May 17 '16 at 09:17
  • @babakfaghihian That would require to parse the request (build the full user object from parameters) in the create method. According to the pattern used, that´s responsibility of the controller. I have seen that parsing in a separate method in the service (user.service -> buildUser(parametersArray) ) for easy reusing, but I think the service should not worry about that. – yawmoght May 29 '16 at 14:32
  • Instead of the EntityManeger, we should type-hint the interface EntityManagerInterface. – Mohammad.Kaab Apr 26 '20 at 15:52
10

If you take a look at the repository pattern http://martinfowler.com/eaaCatalog/repository.html ,

it is stated that repositories uses a "collection-like interface".

Later, it is also written " Objects can be added to and removed from the Repository, as they can from a simple collection of objects".

I'm not saying this is a bible, but there is conceptually nothing wrong to see a repository like a collection that you can query. But as it is a collection, you can add, remove, ... In fact, the ObjectRepository should implement Doctrine\Common\Collection :)

On the other hand, the most important is not to mess reads and writes, as CQS says. That's maybe why they didn't allow directly that, to avoid abuses and read/write mix.

EDIT: I should have talked about flush. This should not be made in the repository itself, as it might break transactional consistency.

You'd better move the flush call to something that wraps the whole business transaction logic (a command bus handling a command f.e?)

Florian Klein
  • 8,692
  • 1
  • 32
  • 42
  • 1
    I think you might be confusing a "generic repository" which is a layer to isolate access to databases, with the "repository" in the context of the OP's question. A generic repository in a symfony/doctrine world is actually the ORM not the Entity Repositories in question. – Ramy Nasr Mar 31 '17 at 15:10
  • Can you elaborate on what is a "generic repository" then? My answer was directly addressed for the doctrine ORM repository pattern the OP was talking about – Florian Klein Apr 02 '17 at 07:55
  • 1
    The entityManager in doctrine ORM can be considered as the "generic repository" layer in that example. The class [reads](http://www.doctrine-project.org/api/orm/2.5/source-class-Doctrine.ORM.EntityManager.html#35), "It is a facade to all different ORM subsystems such as UnitOfWork, Query Language and Repository API". The [EntityRepository](http://www.doctrine-project.org/api/orm/2.5/class-Doctrine.ORM.EntityRepository.html) on the other hand, does not implement any `save()` methods, only `find*()`. Thanks. – Ramy Nasr Apr 03 '17 at 15:38
  • 1
    I wouldn't consider the ORM a generic repository. Doctrine is an ORM. There are two different topics here. The first is Doctrine repositories which are only for finding entities, and you should not flush in these. Then there's the Repository Pattern, which is a different concept. A repository, in this sense, is a collection of "things." As @Florian's said, it could just implement doctrine's Collection interface. Then you create specific implementations which can persist if they'd like. It's a way to allow your business logic to use these repository interfaces and be persistence agnostic. – John Pancoast Sep 27 '19 at 20:25
2

Well, how do you get your repository when not using the entityManager? After all, the entities are not going to be saved magically without a connection to the database, so your service must somehow be aware of any kind of connection.

I don't know about the SOA Services, but in my eyes it makes no difference at all if you use $_em->getRepository()->save($entity) or $_em->persist($entity). On the other hand, if you use flush in your repository, you might end up with way more queries than needed as your repository is now aware of the business logic.

I think there is a way to do this "the SOA way", but I guess it's not persisting the entities within the repository.

Sgoettschkes
  • 13,141
  • 5
  • 60
  • 78
  • Very good point on the `flush()` issue. Perhaps a flush param for verbosity? – Jimbo Dec 24 '14 at 10:42
  • You should not flush in Doctrine repositories for other reasons too. It can break transactions or give unexpected results which is likely why Doctrine did not include the ability to flush in repositories. They did this on purpose and you should not add the ability. There is a difference between flushing from the entity manager and flushing from within a doctrine repository. – John Pancoast Sep 27 '19 at 20:40