According to this question How to load Symfony's config parameters from database (Doctrine) I have a similar problem. I need to set the parameter dynamically and I want to provide data from another custom service.
So, I have Event Listener which setting current account entity (by sub-domain or currently logged user)
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use AppBundle\Manager\AccountManager;
use Palma\UserBundle\Entity\User;
/**
* Class CurrentAccountListener
*
* #package AppBundle\EventListener
*/
class CurrentAccountListener {
/**
* #var TokenStorage
*/
private $tokenStorage;
/**
* #var EntityManager
*/
private $em;
/**
* #var AccountManager
*/
private $accountManager;
private $baseHost;
public function __construct(TokenStorage $tokenStorage, EntityManager $em, AccountManager $accountManager, $baseHost) {
$this->tokenStorage = $tokenStorage;
$this->em = $em;
$this->accountManager = $accountManager;
$this->baseHost = $baseHost;
}
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$accountManager = $this->accountManager;
$accountManager->setCurrentAccount( $this->getCurrentAccount($request) );
}
private function getCurrentAccount($request){
if($this->getCurrentAccountByLoggedUser()) {
return $this->getCurrentAccountByLoggedUser();
}
if($this->getCurrentAccountBySubDomain($request) ) {
return $this->getCurrentAccountBySubDomain($request);
}
return;
}
private function getCurrentAccountBySubDomain($request) {
$host = $request->getHost();
$baseHost = $this->baseHost;
$subdomain = str_replace('.'.$baseHost, '', $host);
$account = $this->em->getRepository('AppBundle:Account')
->findOneBy([ 'urlName' => $subdomain ]);
if(!$account) return;
return $account;
}
private function getCurrentAccountByLoggedUser() {
if( is_null($token = $this->tokenStorage->getToken()) ) return;
$user = $token->getUser();
return ($user instanceof User) ? $user->getAccount() : null;
}
}
services.yml
app.eventlistener.current_account_listener:
class: AppBundle\EventListener\CurrentAccountListener
arguments:
- "#security.token_storage"
- "#doctrine.orm.default_entity_manager"
- "#app.manager.account_manager"
- "%base_host%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
And very simply account manager with setter and getter only. If I need access to current account I call
$this->get('app.manager.account_manager')->getCurrentAccount();
Everything works fine.
Now I am trying set some parameter from my service with compiler pass
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParametersCompilerPass implements CompilerPassInterface {
const ACCOUNT_MANAGER_SERVICE_ID = 'app.manager.account_manager';
public function process(ContainerBuilder $container) {
if(!$container->has(self::ACCOUNT_MANAGER_SERVICE_ID)) {
return;
}
$currentAccount = $container->get(self::ACCOUNT_MANAGER_SERVICE_ID)
->getCurrentAccount();
$container->setParameter(
'current_account', $currentAccount
);
}
}
AppBundle.php
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\ParametersCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ParametersCompilerPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}
I got current_account as null every time, no matter what PassConfig I use. Any ideas?
Thank you for your attention.
Compilation pass are executed when you run Symfony for the first time (CLI command or first http request). Once the cache is build (compiled) this code it never gets executed again.
Solution with parameters [I do not recommend this]
If your parameter can change from one to another HTTP Request you should not use a parameter as some services may be initialized before your parameter is ready and others after. Although if this is the way you want to go, you can add an event that listens to the kernel request and modify/set the parameter there. Have a look at https://symfony.com/doc/current/components/http_kernel.html#component-http-kernel-event-table
Current account in the user / session
If currentAccount depends on the user logged, why you do not store that info in the user or session and access to it from your services?
Related
I will start saying I am using Symfony 4.3.4 and Api Platform (called AP from now on). Having said that this how my custom controller (used for AP) looks like:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
class PendCaseController
{
/**
* #Route("/myroute/{id}/pend", name="routeName")
* #ParamConverter("case", class="App\Entity\Cases")
*/
public function __invoke(PendCaseRequest $request, int $id)
{
// do something with the $request
}
}
As you may notice I also have a Request Data Transformer Object and here is a code snippet for it:
declare(strict_types=1);
namespace App\Request;
use App\Interfaces\RequestDTOInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints as Assert;
class PendCaseRequest implements RequestDTOInterface
{
/**
* #var int
*
* #Assert\NotBlank()
* #Assert\NotNull()
* #Assert\Type("integer")
*/
private $param;
public function __construct(Request $request)
{
$data = json_decode($request->getContent(), true);
$this->param = (int) $data['param'];
// ...
}
}
It's suppose (as per docs here) that when the request comes in and an id matching a App\Entity\Cases is found a new attribute named case should be append to my $request object but in my scenario is not happening and I am not sure why or what I am missing.
While debugging and setting a break point at this line $this->param = (int) $data['param']; in my DTO, if I print out $this->attributes I got the following output:
‌Symfony\Component\HttpFoundation\ParameterBag::__set_state(array(
'parameters' =>
array (
),
))
What I am missing here? What is wrong with my approach?
I have found a "solution" here. I end up using a Decorator as suggested by the answer on that post.
My main controller changed into this:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use App\Entity\Cases;
class PendCaseController
{
public function __invoke(PendCaseRequest $request, Cases $case)
{
// do something with the $request
}
}
A decorator was created:
declare(strict_types=1);
namespace App\Decorator;
use App\Controller\CaseWork\Pend\PendCaseController;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Cases;
use App\Request\PendCaseRequest;
class PendCaseDecorator
{
/** #var PendCaseController */
protected $decoratedController;
/** #var EntityManagerInterface */
protected $entityManager;
public function __construct(PendCaseController $controller, EntityManagerInterface $entityManager)
{
$this->decoratedController = $controller;
$this->entityManager = $entityManager;
}
public function __invoke(PendCaseRequest $request, int $id)
{
$object = $this->entityManager->getRepository(Cases::class)->find($id);
if (!$object instanceof Cases) {
throw new NotFoundHttpException('Entity with '.$id.' not found');
}
return $this->decoratedController($request, $object);
}
}
And I had registered it at services.yml:
services:
App\Controller\CaseWork\Pend\PendCaseController: ~
App\Decorator\PendCaseDecorator:
decorates: App\Controller\CaseWork\Pend\PendCaseController
That way I keep using my DTO and pass back a Cases entity object.
I need to access a $_POST variable from a service and I don't want to pass request as a paramter as I think this forces me to use scope: request in the service and I have bad memories from this scope from the past, as this forces a service instance per request, instead of one instance per application.
The thing is that $_POST works all right, but as symfony best practices recommends to avoid using php primitives I ask if there's a better way to do it (avoiding scope: request in service)
If you are using 2.4+ you can use the request_stack, see here.
You would use it like..
services.yml
your.service:
class: FQCN\To\Your\Service
arguments:
- #request_stack
FQCN\To\Your\Service
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class Service
{
/**
* #var RequestStack
*/
private $requestStack;
/**
* #var Request
*/
private $request;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function yourCall()
{
$param = $this->getRequest()->request->get('your-post-parameter');
//...
}
/**
* Get current request object
*
* #return Request
*/
private function getRequest()
{
if (null === $this->request) {
$this->request = $this->requestStack->getCurrentRequest();
}
return $this->request;
}
}
I don't see any problem with doing it like below which is what I did some time ago. You're not injecting whole Request.
CONTROLLER
public function searchByGetAction(Request $request)
{
//.......
$results = $this->productSearchService->findWithSimpleArray(
$request->query
);
//.......
}
public function searchByPostAction(Request $request)
{
//.......
$results = $this->productSearchService->findkWithNestedArray(
json_decode($request->getContent(), true)
);
//.......
}
SERVICE
public function findWithSimpleArray(ParameterBag $searchParameters)
{
$name = $searchParameters->get('name');
$surname = $searchParameters->get('surname');
//.......
}
public function findWithNestedArray($searchParameters = [])
{
$name = isset($searchParameters['name']) ? $searchParameters['name'] : null;
$surname = isset($searchParameters['surname']) ? $searchParameters['surname'] : null;
//.......
}
I want to return all Logged in users of my application and render it in my Dashboard. The user_id and user_name should be retrieved from the session (I am using an external LDAP Library for authentication)
I have created a field in the database called lastActivity which will contain the last login time and then I can query the database for lastActivity display users logged in in the last 2 minutes.
ActivityListener.php
<?php
namespace Bnpp\SecurityBundle\EventListener;
use Doctrine\ORM\EntityManager;
//use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Acme\SecurityBundle\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Registry;
/**
* Listener that updates the last activity of the authenticated user
*/
class ActivityListener
{
protected $securityContext;
protected $entityManager;
public function __construct(SecurityContext $securityContext, EntityManager $entityManager)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
}
/**
* Update the user "lastActivity" on each request
* #param FilterControllerEvent $event
*/
public function onCoreController(FilterControllerEvent $event)
{
// Check that the current request is a "MASTER_REQUEST"
// Ignore any sub-request
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
// Check token authentication availability
if ($this->securityContext->getToken()) {
$user = $this->securityContext->getToken()->getUser();
if ( ($user instanceof User) && !($user->isActiveNow()) ) {
$user->setLastActivity(new \DateTime('now'));
$this->entityManager->flush($user);
}
}
}
}
Services.yml
services:
activity_listener:
class: Bnpp\SecurityBundle\EventListener\ActivityListener
arguments: [#security.context, #doctrine.orm.entity_manager]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
User Entity
<?php
namespace Acme\SecurityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* User
*
* #ORM\Table(name="users")
* #ORM\Entity(repositoryClass="Acme\SecurityBundle\Entity\UserRepository")
*/
class User implements UserInterface
{
/**
* #var \DateTime
* #ORM\Column(name="LASTACTIVITY", type="datetime")
*/
private $lastActivity;
/**
* #return bool whether the user is active or not
*/
public function isActiveNow()
{
$delay = new\DateTime('2 minutes ago');
return($this->getlastActivity()>$delay);
}
/**
* Set lastActivity
*
* #param\Datetime $lastActivity
* #return User
*/
public function setlastActivity($lastActivity)
{
$this->lastActivity = $lastActivity;
return $this;
}
/**
* Get lastActivity
*
* #return \DateTime
*/
public function getlastActivity()
{
return $this->lastActivity;
}
}
There is a great post here: List online users.
You can create a Listener that listens on the kernel.controller event and updates a user field lastActivity every time a user is active. You can check lastActivity < now()- 2 minutes and update lastActivity timestamp.
Also: Implementing user activity in symfony 2
Here is how to do it
Note: If you're not using FOSUserBundle, see Edit below.
1 Add this to your User Entity
/**
* Date/Time of the last activity
*
* #var \Datetime
* #ORM\Column(name="last_activity_at", type="datetime")
*/
protected $lastActivityAt;
/**
* #param \Datetime $lastActivityAt
*/
public function setLastActivityAt($lastActivityAt)
{
$this->lastActivityAt = $lastActivityAt;
}
/**
* #return \Datetime
*/
public function getLastActivityAt()
{
return $this->lastActivityAt;
}
/**
* #return Bool Whether the user is active or not
*/
public function isActiveNow()
{
// Delay during wich the user will be considered as still active
$delay = new \DateTime('2 minutes ago');
return ( $this->getLastActivityAt() > $delay );
}
2 Create Event Listener
<?php
namespace Acme\UserBundle\EventListener;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
/**
* Listener that updates the last activity of the authenticated user
*/
class ActivityListener
{
protected $securityContext;
protected $userManager;
public function __construct(SecurityContext $securityContext, UserManagerInterface $userManager)
{
$this->securityContext = $securityContext;
$this->userManager = $userManager;
}
/**
* Update the user "lastActivity" on each request
* #param FilterControllerEvent $event
*/
public function onCoreController(FilterControllerEvent $event)
{
// Check that the current request is a "MASTER_REQUEST"
// Ignore any sub-request
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
// Check token authentication availability
if ($this->securityContext->getToken()) {
$user = $this->securityContext->getToken()->getUser();
if ( ($user instanceof UserInterface) && !($user->isActiveNow()) ) {
$user->setLastActivityAt(new \DateTime());
$this->userManager->updateUser($user);
}
}
}
}
3 Declare event Listener as a service
parameters:
acme_user.activity_listener.class: Acme\UserBundle\EventListener\ActivityListener
services:
acme_user.activity_listener:
class: %acme_user.activity_listener.class%
arguments: [#security.context, #fos_user.user_manager]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
And you're good to go!
Edit (without FOSUserBundle)
1 Add this to your User Entity
Same as Step 1 Above
2 Create Event Listener
<?php
namespace Acme\UserBundle\EventListener;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Doctrine\ORM\EntityManager;
use Acme\UserBundle\Entity\User;
/**
* Listener that updates the last activity of the authenticated user
*/
class ActivityListener
{
protected $securityContext;
protected $entityManager;
public function __construct(SecurityContext $securityContext, EntityManager $entityManager)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
}
/**
* Update the user "lastActivity" on each request
* #param FilterControllerEvent $event
*/
public function onCoreController(FilterControllerEvent $event)
{
// Check that the current request is a "MASTER_REQUEST"
// Ignore any sub-request
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
// Check token authentication availability
if ($this->securityContext->getToken()) {
$user = $this->securityContext->getToken()->getUser();
if ( ($user instanceof User) && !($user->isActiveNow()) ) {
$user->setLastActivityAt(new \DateTime());
$this->entityManager->flush($user);
}
}
}
}
3 Declare event Listener as a service
parameters:
acme_user.activity_listener.class: Acme\UserBundle\EventListener\ActivityListener
services:
acme_user.activity_listener:
class: %acme_user.activity_listener.class%
arguments: [#security.context, #doctrine.orm.entity_manager]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
And you're good to go!
As I can't comment on posts, I'd still like to give a remark on the answer by Mick via this answer.
Since Symfony 2.6 the SecurityContext class is deprecated and, in this case, the TokenStorage class should be used instead.
Thus, the services.yml would be as follows:
services:
acme_user.activity_listener:
class: %acme_user.activity_listener.class%
arguments: ['#security.token_storage', '#doctrine.orm.entity_manager']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
And, instead of
use Symfony\Component\Security\Core\SecurityContext;
One should
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
(also replace the SecurityContext inside the class with the TokenStorage class)
Then, on line 38, the token availability would be checked using
$this->tokenStorage->getToken()
And, on line 39, the user instance would be obtained using
$this->tokenStorage->getToken()->getUser()
In Symfony 4 I solved the problem in the following way.
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
class ActivitySubscriber implements EventSubscriberInterface {
private $em;
private $security;
public function __construct(
EntityManagerInterface $em, Security $security) {
$this->em = $em;
$this->security = $security;
}
public function onTerminate() {
$user = $this->security->getUser();
if (!$user->isActiveNow()) {
$user->setLastActivityAt(new \DateTime());
$this->em->persist($user);
$this->em->flush($user);
}
}
public static function getSubscribedEvents() {
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::TERMINATE => [['onTerminate', 20]],
];
}
}
For Symfony3.4 (4), I used EntityManagerInterface to update user, and Security to get user, following codes worked for me :
app/config/services.yml
AppBundle\Service\ActivityListener:
tags:
- { name: 'kernel.event_listener', event: 'kernel.controller', method: onCoreController }
Service/ActivityListener.php
<?php
namespace AppBundle\Service;
use AppBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Security\Core\Security;
class ActivityListener
{
private $em;
private $security;
public function __construct(EntityManagerInterface $em, Security $security)
{
$this->em = $em;
$this->security = $security;
}
public function onCoreController(FilterControllerEvent $event)
{
// Check that the current request is a "MASTER_REQUEST"
// Ignore any sub-request
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
// Check token authentication availability
if ($this->security->getToken()) {
$user = $this->security->getToken()->getUser();
if ( ($user instanceof User) && !($user->isActiveNow()) ) {
$user->setLastActivityAt(new \DateTime());
$this->em->persist($user);
$this->em->flush($user);
}
}
}
}
Update for Symfony 3.4
1. Add this to your User Entity
Same as Step 1 Above
2. Create Event Listener
<?php
namespace Acme\UserBundle\EventListener;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Doctrine\ORM\EntityManager;
use Acme\UserBundle\Entity\User;
/**
* Listener that updates the last activity of the authenticated user
*/
class ActivityListener
{
protected $tokenContext;
protected $doctrine;
public function __construct(TokenyContext $tokenContext, $doctrine)
{
$this->tokenContext= $tokenContext;
$this->doctrine= $doctrine;
}
/**
* Update the user "lastActivity" on each request
* #param FilterControllerEvent $event
*/
public function onCoreController(FilterControllerEvent $event)
{
// Check that the current request is a "MASTER_REQUEST"
// Ignore any sub-request
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
// Check token authentication availability
if ($this->tokenContext->getToken()) {
$user = $this->tokenContext->getToken()->getUser();
if ( ($user instanceof User) && !($user->isActiveNow()) ) {
$user->setLastActivityAt(new \DateTime());
$this->doctrine->getManager()->flush($user);
}
}
}
}
3. Declare event Listener as a service
parameters:
acme_user.activity_listener.class: Acme\UserBundle\EventListener\ActivityListener
services:
acme_user.activity_listener:
class: %acme_user.activity_listener.class%
arguments: ['#security.token_storage', '#doctrine']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
I have the entity (such as below). I want to set some default values while creating.
As you can see in __construct, it is easy to set the $name (string), but how can I set the $group? (for example I know that there is a group in database with id=122)
/**
* #ORM\Entity
*/
class Person {
private $id;
/** #ORM\Column(type="string") */
private $name;
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
// ... setters/getters
//construct default Person
public function __construct()
{
$this->setName("Mike");
$this->setGroup($EXISTING_GROUP_FROM_MY_DB); // <<--------------
}
}
I agree with moonwave99 that this is poor design. Here you are trying to access the database (through the Doctrine service) from a place that is not container-aware (i.e. it does not, and should not, know about Doctrine).
I had a similar issue recently... pretty much the same exact issue, actually. But I didn't want this logic to be inside the controller. So I wrote a service to take care of the User creation. And I gave that service access to the only other service it needed: Doctrine.
Here's an example, where a User is created with all available Roles:
namespace MyBundle\Entity;
class UserFactory
{
private $doctrine;
public function __construct($doctrine)
{
$this->doctrine = $doctrine;
}
public function generateNewUser($email, $password)
{
$user = new User();
// Since you have access to the Doctrine service, you can use $this->doctrine
// to do anything you would normally do in your controller with $this->getDoctrine()
$roles = $this->doctrine->getEntityManager()->getRepository("MyBundle:Role")->findAll();
foreach ($roles as $role)
{
$user->addRole($role);
}
return $user;
}
}
Now register that service in config.yml or services.yml, remembering to pass the Doctrine service to it:
services:
mybundle.factory.user:
class: MyBundle\Entity\UserFactory
arguments: ['#doctrine']
And that's it... Now, in your controller, you can create a new User by doing:
public function MyController()
{
$user = $this->get("mybundle.factory.user")->generateNewUser("someone#email.com", "password123");
}
The recommended method is to require the associated Entity object within the constructor arguments, optionally in combination with a Factory such as the Entity Repository, to supply the Group Entity during instantiation. This ensures the entity is always in a valid state.
src/Entity/Person.php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\PersonRepository")
*/
class Person
{
//...
public function __construct($name, Group $group)
{
$this->setName($name);
$this->setGroup($group);
}
//...
}
src/Repsotory/PersonRepository.php
namespace App\Repsotory;
use App\Entity\Group;
use App\Entity\Person;
class PersonRepository
{
const DEFAULT_GROUP = 122;
public function create($name, Group $group = null)
{
if (null === $group) {
$group = $this->_em->getReference(Group::class, self::DEFAULT_GROUP);
}
$person = new Person($name, $group);
$this->_em->persist($person);
return $person;
}
}
This allows you to rely solely on the Doctrine ORM Entity Manager to maintain the default Group association.
$person = $em->getRepository(Person::class)->create('Mike');
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$em->flush();
This approach can be extended upon in Symfony to use Query services instead of the doctrine entity repository, to provide a central location that handles the instantiation of the entities.
In Symfony 3.4+ you can use Repository
services
to provide dependency injection for the repository, instead of using
the EntityManagerInterface.
src/Service/PersonCreateQuery.php
namespace App\Service;
use App\Entity\Group;
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
class PersonCreateQuery
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function __invoke($name)
{
$group = $this->em->getReference(Group::class, 122);
$person = new Person($name, $group);
$this->em->persist($person);
return $person;
}
}
Now you can use dependency injection to retrieve the Query service and use it as desired, such as with a Symfony Form or Controller.
namespace App\Controller;
use App\Service\PersonCreateQuery;
class PersonController
{
public function createAction(PersonCreateQuery $query)
{
$person = $query('Mike');
$this->getDoctrine()->getManager()->flush();
//...
}
}
Note: Usages of $em->getReference() can be replaced with $em->find(). Using $em->getReference() will prevent a query to the database but will throw an exception if the reference is invalid, while using $em->find() will return null instead.
Another approach is to use either Lifecycle Callbacks in the entity or an Event Listener to do more complex functionality. However, this will cause your entity to be instantiated in an invalid state until it is persisted.
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Person
{
const DEFAULT_GROUP = 122;
/** #ORM\Column(type="string") */
private $name = 'Mike';
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
//....
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
/**
* #param LifecycleEventArgs $event
* #ORM\PrePersist
*/
public function onPrePersist(LifecycleEventArgs $event)
{
if (!$this->group instanceof Group) {
/** set default group if not specified */
$group = $event->getEntityManager()->getReference(Group::class, self::DEFAULT_GROUP);
$this->setGroup($group);
}
}
}
Now when you persist a Person entity it will add the group if it was not explicitly set elsewhere.
$person = new Person();
$person->setName('Foo Bar');
$em->persist($person); //persist or do nothing if already persisted
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$groupPerson = $group->getPerson();
echo $groupPerson->getName(); //outputs: Foo Bar
$em->flush(); //save to database
For sanity here are the links to the docs for the doctrine events:
Doctrine 2 - Events
Doctrine 2 - Lifecycle Callbacks
Symfony - Doctrine Lifecycle Callbacks
How to use entity as service in doctrine (Using Symfony 2.1).
Example usage:
<?php
namespace MyNamespace;
class MyEntity
{
protected $container = NULL;
public function __construct($container)
{
$this->container = $container;
}
/**
* #ORM\PrePersist
*/
public function()
{
// Must call to container and get any parameters
// for defaults sets entity parameters
$this->container->get('service.name');
}
}
As a result, I need to get access to the entire container.
EDIT: THIS IS NOT THE PREFERRED WAY, it's the only way to get service container inside an entity, it's not a good practice, it should be avoided, but this just answers the question.
In case you still want the container and/or repository you can extend a base abastractEntity like this:
<?php
namespace Acme\CoreBundle\Entity;
/**
* Abstract Entity
*/
abstract class AbstractEntity
{
/**
* Return the actual entity repository
*
* #return entity repository or null
*/
protected function getRepository()
{
global $kernel;
if ('AppCache' == get_class($kernel)) {
$kernel = $kernel->getKernel();
}
$annotationReader = $kernel->getContainer()->get('annotation_reader');
$object = new \ReflectionObject($this);
if ($configuration = $annotationReader->getClassAnnotation($object, 'Doctrine\ORM\Mapping\Entity')) {
if (!is_null($configuration->repositoryClass)) {
$repository = $kernel->getContainer()->get('doctrine.orm.entity_manager')->getRepository(get_class($this));
return $repository;
}
}
return null;
}
}
An entity is a data model and should only hold data (and not have any dependencies on services). If you want to modify your model in case of a certain event (PrePersist in your case) you should look into making a Doctrine listener for that. You can inject the container when defining the listener:
services:
my.listener:
class: Acme\SearchBundle\Listener\YourListener
arguments: [#your_service_dependency_or_the_container_here]
tags:
- { name: doctrine.event_listener, event: prePersist }