Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 3 years ago.
Improve this question
For Symfony projects using Doctrine ORM, I'm used to Anemic Domain Models, with business logic handled in Symfony services. For projects involving heavy business logic, I'm wondering if using Rich Domain Models (which I'm not familiar with) would not be a better solution. I'm not looking for a comparison of RDM vs ADM, as there is already plenty of resources available online to figure out pros and cons of each solution. I'm rather wondering if RDM are suited for Symfony projects. To make my mind, I would like a glimpse of how I should implement RMD in a real world application.
The main questions I have are:
Does RDM really suit Symfony philosophy and best practices?
Does RDM not break the SOLID principle by having models doing too much?
How dependencies are managed by the models?
I'll give a theoretical example to express my concerns in a clearer way.
Lets say we are working on a REST API. We want to implement an user registration. The user registration relies on an external service provider (using an API) and the user creation should result in the creation of some other related entities (we will use a Dummy entity here) and the populate of ElasticSearch indexes in an asynchronous manner using Symfony Messenger.
The happy flow would look like this:
Validation of the request body
Call to the external service provider API to create the user on their side
Insertion of the user data in database
Create user related entities
Populate the ElasticSearch indexes
Send a confirmation email to the user
For practical reasons, the code below is simplified and therefore may not be 100% accurate nor functional. It only aims to demonstrate the differences between the two designs.
Anemic Domain Model implementation
<?php
// src/Entity/User.php
namespace App\Entity;
class User
{
private $id;
private $externalId;
private $email;
private $plainPassword;
private $password;
private $lastName;
private $firstName;
// Include more properties, getters and setters, ...
}
<?php
// src/Service/UserRegistrationService.php
namespace App\Service;
class UserRegistrationService
{
private $passwordEncoder;
private $externalProviderClient;
private $em;
private $bus;
private $mailer;
private $elasticSearch;
private $dummyService;
public function __construct(PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, Mailer $mailer, ElasticSearchService $elasticSearch, DummyService $dummyService)
{
$this->passwordEncoder = $passwordEncoder;
$this->externalProviderClient = $externalProviderClient;
$this->em = $em;
$this->bus = $bus;
$this->mailer = $mailer;
$this->elasticSearch = $elasticSearch;
$this->dummyService = $dummyService;
}
public function register(User $user): User
{
$password = $this->passwordEncoder->encode($user->getPlainPassword());
$externalId = $this->externalProviderClient->registerUser([
'email' => $user->getEmail(),
'firstName' => $user->getFirstName(),
'lastName' => $user->getLastName(),
]);
$user
->setExternalId($externalId)
->setPassword($password)
->setPlainPassword(null)
;
$this->em->persist($user);
$this->em->flush();
$this->bus->dispatch(new UserMessage($user));
return $user;
}
public function completeRegistration(User $user)
{
$dummy = $this->dummyService->createUserDependencies($user);
$this->elasticSearch->populateUser($user);
$this->elasticSearch->populateDummy($dummy);
$this->mailer->sendConfirmationEmail($user);
}
}
<?php
// src/Controller/UserController.php
namespace App\Controller;
class UserController
{
public function registerAction(Request $request, UserRegistrationService $userRegistrationService, Serializer $serializer)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if (!$form->isValid()) {
// Process errors
}
$userRegistrationService->register($user);
return $this->serializer->serialize($user);
}
}
As you can see, the business logic relies on a lot of services, and this is just one of the tiniest and most basic functionality the API has to offer.
Rich Domain Model implementation
<?php
// src/Entity/User.php
namespace App\Entity;
class User
{
private $id;
private $externalId;
private $email;
private $plainPassword;
private $password;
private $lastName;
private $firstName;
// Include more properties, getters and setters, ...
}
<?php
// src/Model/UserRegistration.php
namespace App\Model;
class UserRegistration
{
private $email;
private $plainPassword;
private $lastName;
private $firstName;
private function __construct(string $email, string $plainPassword, string $lastName, string $firstName)
{
$this->email = $email;
$this->plainPassword = $plainPassword;
$this->lastName = $lastName;
$this->firstName = $firstName;
}
// Getters
public static function createFromRequest(Validator $validator, Request $request)
{
$requestData = $request->request->all();
$userRegistration = new self(requestData['email'], $requestData['plainPassword'], $requestData['lastName'], $requestData['firstName']);
$violations = $validator->validate($userRegistration);
if (count($violations) > 0) {
throw new \Exception(); // handle errors
}
return $userRegistration;
}
}
<?php
// src/Model/User.php
namespace App\Model;
class User
{
private $id;
private $externalId;
private $email;
private $oassword;
private $lastName;
private $lastName;
private function __construct(string $id, string $externalId, string $email, string $password, string $lastName, string $firstName)
{
$this->id = $id;
$this->externalId = $externalId;
$this->email = $email;
$this->password = $password;
$this->lastName = $lastName;
$this->firstName = $firstName;
}
// Getters
public static function createFromUserRegistration(Validator $validator, PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, UserRegistration $userRegistration)
{
$password = $passwordEncoder->encodePassword($userRegistration->getPlainPassword());
$externalId = $externalProviderClient->registerUser([
'email' => $userRegistration->getEmail(),
'firstName' => $userRegistration->getFirstName(),
'lastName' => $userRegistration->getLastName(),
]);
$userEntity = (new \App\Entity\User())
->setExternalId($externalId)
->setEmail($userRegistration->getEmail())
->setPassword($password)
->setLastName($userRegistration->getLastName())
->setFirstName($userRegistration->getFirstName())
;
$em->persist($userEntity);
$em->flush();
$id = ;
$user = self::buildFromUserEntity($validator, $userEntity);
$bus->dispatch(new UserMessage($user));
return $user;
}
public static function buildFromUserEntity(Validator $validator, \App\Entity\User $userEntity)
{
$user = new self(
$userEntity->getId(),
$userEntity->getExternalId(),
$userEntity->getEmail(),
$userEntity->getPassword(),
$userEntity->getLastName(),
$userEntity->getFirstName()
);
$violations = $validator->validate($user);
if (count($violations) > 0) {
throw new \Exception(); // handle errors
}
return $user;
}
public function completeRegistration(EntityManager $em, ElasticSearch $elasticSearch, Mailer $mailer)
{
$dummy = new Dummy($this);
$dummy->save($em);
$this->populateElasticSearch($elasticSearch);
$dummy->populateElasticSearch($elasticSearch);
$this->sendConfirmationEmail($mailer);
}
public function populateElasticSearch(ElasticSearch $elasticSearch)
{
$this->elasticSearch->populate($this);
}
public function sendConfirmationEmail(Mailer $mailer)
{
$this->mailer->sendConfirmationEmail($this);
}
public function serialize(Serializer $serializer)
{
return $serializer->serialize($user);
}
}
<?php
// src/Controller/UserController.php
namespace App\Controller;
class UserController
{
public function registerAction(Request $request, Validator $validator, PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, Serializer $serializer)
{
$userRegistration = UserRegistration::createFromRequest($validator, $request);
$user = User::createFromUserRegistration($validator, $passwordEncoder, $externalProviderClient, $em, $bus, $userRegistration);
return $user->serialize($serializer);
}
}
The main problems I see with this implementation are:
Duplicate code between user RMD and entity
Models will get bigger and bigger with each new functionality
Managing dependencies can quickly be a mess
What are your thoughts on this? Is there things I am doing wrong?
I hope you are aware, that this is (obviously) mostly opinion-based, thus you might agree or disagree with it.
I think you have a major misunderstanding of what the Rich Domain Model and the Anemic Domain Model is. But let's start with a few assumptions:
In my understanding, entities in symfony (more precisely in Doctrine's ORM) already are models in most cases. Thus having an additional User model is just unnecessary. And even if you wanted to split, I wouldn't put the exact same fields into the model, but just have the entity as a field. If you happen to copy all of the entities functions to the model, you're doing it wrong. Since by default all fields of an entity should be private, there is no reason to not treat it as a model. (I mean, it already works with objects and not their ids in relations...).
A User model/entity should never be concerned with sending emails, IMHO, because it breaks separation of concerns. Instead there should be something modelling the process in which the email is sent. I find this article describes rather cleanly, how this works. Pay attention to the changes to Shipment and to the CheckoutService -> Checkout.
Ironically, your "Anemic Domain Model"s UserRegistrationService is quite good. The user entity is a bit anemic in that implementation though and it should probably validate the User entity, but apart from that, the service could be renamed to UserRegistration and would fit very fine into RDM. (I agree, that the validation is already being done by the form (a convenience really), but there might be validations that are not about the consistency of the user in itself, but about the user as part of the collection of users in your database / model, or something else).
To summarize: In my view, Symfony can do RDM, and rather well. But the real crux (as always) is to actually choose/design the best model.
Essentially: Anemic means you don't have one place where all things are consistently done, but instead is split in a way which risks consistency/integrity or split separation of concerns to independent units. RDM on the contrary keeps it clustered in semantically sensible places. It doesn't change the fact, that you still want separation of concerns!
Now to answer your questions:
Does RDM really suit Symfony philosophy and best practices?
why not. depends on the modelling though, which might be adapted to fit Symfony's best practices.
Does RDM not break the SOLID principle by having models doing too much?
not generally, if done right. Your implementation definitely does break SOLID, but RDM doesn't have to. Nobody is saying that having a UserRegister and UserCancel and UserUpdate service/model would be wrong. RDM is about keeping the stuff that semantically belongs together in a business process/unit also together in code (which doesn't negate separation of concern or single purpose).
How dependencies are managed by the models?
Since in my view, business processes are models, and will act as services, dependencies are just as they are handled in services. Entities on the other hand should not need services ever*. (there might be some very very special circumstances, in which case you might want to actually have a service (a factory maybe) that manages the creation/updates of the entity)
What are your thoughts on this? Is there things I am doing wrong?
your implementation is, let's say 'unfortunate' in that it isn't RDM in my understanding and (as you realize yourself) breaks SOLID all over the place. So yes to the second part of the question.
Related
This is a question about the event system in Doctrine (within a Symfony project).
I have two classes, User and Visit, that are associated via a many-to-many relationship. I.e. a user can have many visits and a visit can have many users (that attend the visit).
class Visit
{
#[ORM\Column]
protected string $Date;
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: "Visits")]
#[ORM\JoinTable(name: "users_visits")]
protected Collection $Users;
public function __construct()
{
$this->Users = new ArrayCollection();
}
//... other properties and methods omitted
}
class User
{
#[ORM\Column]
protected string $Name;
#[ORM\ManyToMany(targetEntity: Visit::class, inversedBy: "Users")]
#[ORM\JoinTable(name: "users_visits")]
protected Collection $Visits;
public function __construct()
{
$this->Visits = new ArrayCollection();
}
//... other properties and methods omitted
}
I also have an UpdateSubscriber that is supposed to record certain inserts, updates or removals, in a separate sql-table to create an overview over all relevant changes later on.
class UpdateSubscriber implements EventSubscriberInterface
{
public function __construct(private LoggerInterface $logger)
{
}
public function getSubscribedEvents(): array
{
return [
Events::preUpdate,
Events::postPersist,
Events::postRemove,
Events::postFlush
];
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$this->logger->debug('Something has been updated');
if($args->hasChangedField('Date')){
$this->logger->debug('The date has been changed.');
}
if($args->hasChangedField('Users')){
$this->logger->debug('It was the Users field');
}
}
// ... other methods emitted
I have gotten this system to work, but when I run this test code
$visitRepo = $this->om->getRepository(Visit::class);
$userRepo = $this->om->getRepository(User::class);
// you can assume that visit 7 and user 8 already exist in the database
$v = $visitRepo->find(7);
$u = $userRepo->find(8);
$v->setDate('2022-01-05');
$this->om->flush();
$v->addUser($u);
$this->om->flush();
The test code works without errors and I can see a new row in the sql-table "users_visits". I can also see the date for visit 7 has been changed to 2022-01-05.
BUT: Checking the log I can only find
Something has been updated.
The date has been changed.
Something has been updated.
There is no "It was the Users field". Using my debugging tools I can see that the EntityChangeSet is empty during preUpdate() during the $v->addUser($u), which is weird and unexpected.
I have extensively been reading the docs for the event PreUpdate but there is no mentioning of why changes to associated collections are not shown in the EntityChangeSet or how I could track those changes in an EventSubscriber.
Do I have to go via the rather cumbersome UnitOfWork during the onFlushEvent? Or this there something I have been missing?
In case I myself or somebody else reads this, my pretty dirty solution was to collect all currently updated collections and then check if the property ("Users" in this case) matched on of these.
$updatedCollectionNames = array_map(
function (PersistentCollection $update) {
return $update->getMapping()['fieldName'];
},
$args->getEntityManager()->getUnitOfWork()->getScheduledCollectionUpdates()
);
// and later
if (in_array('Users', $updatedCollectionNames, true)){
$this->logger->debug('It was the Users field');
}
This seems dirty and contrived, but it will do for now until somebody has a better proposal.
We're trying to find the best way to implement dependency injection in a Symfony project with a quite specific problematic.
At user level, our application rely on an "Account" doctrine entity which is loaded with the help of the HTTP_HOST global against a domain property (multi-domain application). Going on the domain example.domain.tld will load the matching entity and settings.
At the devops level, we also need to do batch work with CLI scripts on many accounts at the same time.
The question we are facing is how to write services that will be compatible with both needs?
Let's illustrate this with a simplified example. For the user level we have this and everything works great:
Controller/FileController.php
public function new(Request $request, FileManager $fileManager): Response
{
...
$fileManager->addFile($file);
...
}
Service/FileManager.php
public function __construct(AccountFactory $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function __construct(RequestStack $requestStack, AccountRepository $accountRepository)
{
$this->requestStack = $requestStack;
$this->accountRepository = $accountRepository;
}
public function createAccount()
{
$httpHost = $this->requestStack->getCurrentRequest()->server->get('HTTP_HOST');
$account = $this->accountRepository->findOneBy(['domain' => $httpHost]);
if (!$account) {
throw $this->createNotFoundException(sprintf('No matching account for given host %s', $httpHost));
}
return $account;
}
Now if we wanted to write the following console command, it would fail because the FileManager is only accepting an AccountFactory and not the Account Entity.
$accounts = $accountRepository->findAll();
foreach ($accounts as $account) {
$fileManager = new FileManager($account);
$fileManager->addFile($file);
}
We could tweak in the AccountFactory but this would feel wrong...
In reality this is even worse because the Account dependency is deeper in services.
Does anyone have an idea how to make this properly ?
As a good practice, you should create an interface for the FileManager and set this FileManagerInterface as your dependency injection (instead of FileManager).
Then, you can have different classes that follow the same interface rules but just have a different constructor.
With this approach you can implement something like:
Service/FileManager.php
interface FileManagerInterface
{
// declare the methods that must be implemented
public function FileManagerFunctionA();
public function FileManagerFunctionB(ParamType $paramX):ReturnType;
}
FileManagerInterface.php
class FileManagerBase implements FileManagerInterface
{
// implement the methods defined on the interface
public function FileManagerFunctionA()
{
//... code
}
public function FileManagerFunctionB(ParamType $paramX):ReturnType
{
//... code
}
}
FileManagerForFactory.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountFactory $account)
{
// your code here using the account factory object
}
// additional code that is needed for this implementation and that is not on the base class
}
FileManagerAnother.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountInterface $account)
{
// your code here using the account object
}
// additional code that is needed for this implementation and that is not on the base class
}
Ans last but not least:
Controller/FileController.php
public function new(Request $request, FileManagerInterface $fileManager): Response
{
// ... code using the file manager interface
}
Another approach that also looks correct is, assuming that FileManager depends on an AccountInstance to work, changes could be made to your FileManager dependency to have the AccountInstance as a dependency instead of the Factory. Just Because in fact, the FileManager does not need the factory, it needs the result that the factory generates, so, automatically it is not FileManager's responsibility to carry the entire Factory.
With this approach you will only have to change your declarations like:
Service/FileManager.php
public function __construct(AccountInterface $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function createAccount():AccountInterface
{
// ... your code
}
I am developing my first Symfony 4 application and I migrating from Symfony 2+ and symfony 3+.
Right now I am developing a back-end and all of my entity classes have a addedBy() and updatedBy() methods where I need to record the current logged in administrator.
I would like to have something like an event listener where I do not have to set those methods in all of my controllers.
How to accomplish this?
First, to simplify matters and help down the road I would create an interface this user-tracking entities would need to comply with:
interface UserTracking
{
public function addedBy(UserInterface $user);
public function updatedby(UserInterface $user);
public function getAddedBy(): ?UserInterface;
public function getUpdatedBy(): ?UserInterface;
}
Then you can create a Doctrine event listener, and inject the Security component there:
class UserDataListener
{
protected $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersist(LifecycleEventArgs $event): void
{
$entity = $event->getObject();
$user = $this->security->getUser();
// only do stuff if $entity cares about user data and we have a logged in user
if ( ! $entity instanceof UserTracking || null === $user ) {
return;
}
$this->setUserData($entity, $user);
}
private function preUpdate(LifecycleEventArgs $event) {
$this->prePersist($event);
}
private function setUserData(UserTracking $entity, UserInterface $user)
{
if (null === $entity->getAddedBy()) {
$entity->addedBy($user);
}
$entity->updatedBy($user);
}
}
You'd need to tag the listener appropriately, so it triggers on prePersist and preUpdate:
services:
user_data_listener:
class: App\Infrastructure\Doctrine\Listener\UserDataListener
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate }
While the above should work, I believe it's generally not such a great idea to use Doctrine events this way, since you are coupling your domain logic with Doctrine, and are hiding changing under a layer of magic that may not be immediately evident for other developers working with your application.
I'd put the createdBy as a constructor parameter, and set updateBy explicitly when needed. It's just one line of code each time, but you gain clarity and expressiveness, and you have a simpler system with less moving parts.
class FooEntity
{
private $addedBy;
private $updatedBy;
public function __construct(UserInterface $user)
{
$this->addedBy = $user;
}
public function updatedBy(UserInterface $user)
{
$this->updatedBy = $user;
}
}
This expresses much better what's happening with the domain, and future coders in your application do no have to dig around for the possible extensions you may have installed and enabled, or what events are being triggered.
You probably do not want to reinvent the wheel. There is the Blameable Doctrine Extension that already does that. You can install it with :
composer require antishov/doctrine-extensions-bundle
As there is already a recipe to configure it. Then you can activate the extension and then use it with something like :
Specific documentation for the Blameable extension can be found here
use Gedmo\Mapping\Annotation as Gedmo;
class Post {
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="App\Entity\User")
*/
private $createdBy;
}
I preferred double layer models (mapper and model) over doctrine in my zend framework 2 project and trying to make them work little bit like doctrine so I can access relational data from the models (entities). Following example demonstrates what I am trying to achieve.
class User
{
protected $userTable;
public $id;
public $name;
public function __construct($userTable)
{
$this->userTable = $userTable
}
public function getUserArticles()
{
return $this->userTable->getUserArticles($this->id);
}
}
Problem is I cannot inject my user table in my user model, because table gateway uses model class as array object prototype which gets later injected to create user table gateway (mapper).
I don't want to inject service manager in my models as it is considered as a bad practice. How can I inject my user table in my user model? is it possible? what is the best way to achieve what I am trying to do
What you are trying to do is mix two design patterns: Active Record and Data Mapper.
If you take a look at the Data Mapper pattern, you have the Mapper that accesses both the Model and the database. The Model is passive - usually does not call external resources (it's a POPO - Plain Old PHP Object).
A solution for your issue is to inject the related information into the Model, thus keeping the Model only as a data structure.
Here is a working scenario for an MVC application:
Controller - used for input validation & retrieving data from services
<?php
...
public function viewAction()
{
$id = (int) $this->params()->fromQuery('id');
$service = $this->getServiceLocator()->get('your-user-service-name');
$user = $service->getUser($id);
...
}
Service - used for executing the business logic; calls multiple Data Mappers
<?php
...
public function getUser($id)
{
// get user
$mapper = $this->getServiceLocator()->get('your-user-mapper');
$user = $mapper->getUserById($id);
// get articles
$article_mapper = $this->getServiceLocator()->get('your-article-mapper');
$user->articles = $article_mapper->getArticlesByUser($id);
return $user;
}
Data Mapper - used to manipulate one type of Domain entity - it should be composed with a tableGateway if you are accessing the database
<?php
...
public function getUserById($id)
{
$select = $this->tableGateway->getSql()->select();
$select = $select->where(array('id' => $value));
$row = $this->tableGateway->selectWith($select)->current();
return $row;
}
Domain Model - used for data representation
<?php
...
class User
{
public $name; // user name
...
public $articles; // holds the user articles
}
Advantages
Passive Models are easy to read - understand the data structure and it's relations.
Passive Models are easy to test - you don't need external dependencies.
You separate the persistence layer from the Domain layer.
Hope this helps!
You should not inject your mapper into your model, that's exactly the other way around. Important for you to understand is the way the relations work and models shouldn't have any knowledge how their data is mapped to a persistency framework.
You refer to Doctrine, so I'd suggest you also look at how Doctrine solves this problem. The way they do it is via a Proxy. A proxy is a generated class (you need to write your own generator or write all proxies yourself) which extends the model and have the mapper injected:
class Foo
{
protected $id;
protected $name;
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
}
class ProxyFoo extends Foo
{
protected $mapper;
public function __construct(FooMapper $mapper)
{
$this->mapper = $mapper;
}
public function getName()
{
if (null === $this->name) {
$this->load();
}
return parent::getName();
}
protected function load()
{
$data = $this->mapper->findById($this->id);
// Populate this model with $data
}
}
My suggestion: look either at the default mapper pattern Zend Framework 2 applies and forget lazy loading, or just use Doctrine. This is too much work to get this done properly.
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?
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
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.
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));
}