Symfony 2.0 getting service inside entity
Asked Answered
B

3

38

Im seraching over and cannot find answer. I have database role model in my application. User can have a role but this role must be stored into database.

But then user needs to have default role added from database. So i created a service:

<?php

namespace Alef\UserBundle\Service;

use Alef\UserBundle\Entity\Role;

/**
 * Description of RoleService
 *
 * @author oracle
 */
class RoleService {

    const ENTITY_NAME = 'AlefUserBundle:Role';

    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);
    }

    public function addRole($name, $role) {
        if (($newrole = findRoleByRole($role)) != null)
            return $newrole;
        if (($newrole = findRoleByName($name)) != null)
            return $newrole;

        //there is no existing role
        $newrole = new Role();
        $newrole->setName($name);
        $newrole->setRole($role);

        $em->persist($newrole);
        $em->flush();

        return $newrole;
    }

    public function getRoleByName($name) {
        return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
    }

    public function getRoleByRole($role) {
        return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
    }

}

my services.yml is:

alef.role_service:
    class: Alef\UserBundle\Service\RoleService
    arguments: [%doctrine.orm.entity_manager%]

And now I want to use it in two places: UserController and User entity. How can i get them inside entity? As for controller i think i just need to:

$this->get('alef.role_service');

But how to get service inside entity?

Barefoot answered 26/4, 2012 at 9:29 Comment(0)
V
45

You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.

What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:

http://symfony.com/doc/current/book/security.html

Viceregent answered 26/4, 2012 at 12:25 Comment(0)
A
39

The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.

However, I have found there is a way to do it that does not involve messing with the global kernel.

Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.

EventListeners can be made as services which you inject other services into.

Add to app/config/config.yml:

services:
     example.listener:
           class: Alef\UserBundle\EventListener\ExampleListener
     arguments:
           - '@alef.role_service'
     tags:
           - { name: doctrine.event_listener, event: postLoad }

Add to your Entity:

 use Alef\UserBundle\Service\RoleService;

 private $roleService;

 public function setRoleService(RoleService $roleService) {
      $this->roleService = $roleService;
 }

And add the new EventListener:

namespace Alef\UserBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;

class ExampleListener
{
     private $roleService;

     public function __construct(RoleService $roleService) {
         $this->roleService = $roleService;
     }

     public function postLoad(LifecycleEventArgs $args)
     {
         $entity = $args->getEntity();
         if(method_exists($entity, 'setRoleService')) {
             $entity->setRoleService($this->roleService);
         }
     }
}

Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.

Amalita answered 29/9, 2015 at 18:54 Comment(8)
That's the best solution and most precise answer for question. I hate answers like "you can't", "it is so wrong design pattern" or "It will ruin everything, you moron!" written by gurus of nothing. "Worse is (sometimes) better" ;) Thanks again.Eyeglass
Sadly, "wrong design pattern", "MCV model", "Seperate Business Layer", etc.. go out of the window when the 'boss' wants the thing done "right now". Thanks for the answer @Amalita , it is what I was looking for.Ardussi
The thing I dislike in case of EventListener is that it is called for every entity you have.Andrewandrewes
I believe this should be selected answerObeded
This should be the selected answerForeside
As to the comment about not liking this answer because the event listener is called for every entity you have, I will also point out that you are already going against the intent of Symfony to have services inside entities. Really the answer is to redesign your app to not use services in entities if you want it done the right way. This is simply a quick and dirty solution that is the best way I've found to do what you aren't really supposed to do.Amalita
What is "the proper way? Entities have some methods (well, marked with notations like @ORM\PostPersist) to be used when a mutation occurs, so IMHO it's the best place to add code if you need it.Laurent
Is it adding an EntitySubscriber class aside and never use built-in entity events? I found this and works great. github.com/ld-web/medium-sf-di-solid-geocodingLaurent
U
1

Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .

It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)

As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:

in config/services.yaml:

services:
    Alef\UserBundle\EventListener\ExampleListener:
        tags:
           - { name: doctrine.event_listener, event: postLoad }

ExampleListener:

namespace Alef\UserBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;

class ExampleListener
{

    public function postLoad(LifecycleEventArgs $postLoad): void
    {
        $entity = $postLoad->getEntity();
        if ($entity instanceof User) {
            $repository = ;
            $entity->roleRepository(
                $postLoad->getEntityManager()->getRepository(Role::class)
            );
        }
    }
}

And in your Entity (or in a trait if you use it in more than one entity):


    use Alef\UserBundle\Service\RoleService;

    /** @internal bridge for legacy schema */
    public function roleRepository(?RoleRepository $repository = null) {
        static $roleRepository;
        if (null !== $repository) {
            $roleRepository = $repository;
        }
        return $roleRepository;
    }

    public function getRoleByName($name) {
        return $this->roleRepository()->findBy(array('name' => $name));
    }
Underpinning answered 2/8, 2021 at 14:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.