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?

Kaminari
  • 1,387
  • 3
  • 17
  • 32

3 Answers3

46

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

Cerad
  • 48,157
  • 8
  • 90
  • 92
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.

Kai
  • 3,803
  • 1
  • 16
  • 33
  • 21
    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. – Astinus Eberhard May 06 '16 at 10:44
  • 14
    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 @Kai , it is what I was looking for. – Zuhayer Tahir Jul 13 '17 at 11:15
  • 3
    The thing I dislike in case of `EventListener` is that it is called for every entity you have. – Roman Newaza Oct 04 '18 at 11:29
  • 1
    I believe this should be selected answer – Kyeno Feb 11 '19 at 13:37
  • This should be the selected answer – Chuck Norris Jul 07 '20 at 08:07
  • 1
    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. – Kai Jul 07 '20 at 16:42
  • 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. – Miguel Gil Martínez Aug 13 '21 at 16:04
  • Is it adding an EntitySubscriber class aside and never use built-in entity events? I found this and works great. https://github.com/ld-web/medium-sf-di-solid-geocoding – Miguel Gil Martínez Aug 13 '21 at 17:41
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));
    }
Asenar
  • 6,732
  • 3
  • 36
  • 49