I'm studying Symfony and I'm trying with Dispatch Event Component.
I Understand that there are
Dispatcher (component)
Event ( that pass information to Listener )
Listener ( the logic )
I created a simple example in my route
/**
* #Route("/evento", name="evento")
*/
public function eventoAction()
{
$dispatcher = new EventDispatcher();
$listener = new StoreListener($this->get('mailer'));
$dispatcher->addListener(StoreEvents::STORE_ORDER,array($listener,'onFooAction'),25);
$order = new Ordini();
$order->setName('ordine nuovo -'.rand(0,100));
$em = $this->getDoctrine()->getManager();
$em->persist($order);
$em->flush();
$event = new FilterOrderEvent($order);
$dispatcher->dispatch(StoreEvents::STORE_ORDER,$event);
return $this->render('::event.html.twig');
}
StoreEvent with Contain the Const String
namespace AppBundle\Events;
final class StoreEvents
{
/**
* The store.order event is thrown each time an order is created
* in the system.
*
* The event listener receives an
* Acme\StoreBundle\Event\FilterOrderEvent instance.
*
* #var string
*/
const STORE_ORDER = 'store.order';
}
The Event that I pass to Listener
<?php
namespace AppBundle\Events;
use AppBundle\Entity\Ordini;
use Symfony\Component\EventDispatcher\Event;
class FilterOrderEvent extends Event
{
protected $order;
public function __construct(Ordini $order)
{
$this->order = $order;
}
/**
* #return Ordini
*/
public function getOrder()
{
return $this->order;
}
}
And The Listener
<?php
namespace AppBundle\Events;
use Symfony\Component\EventDispatcher\Event;
class StoreListener
{
protected $mail;
/**
* StoreListener constructor.
* #param $mail
*/
public function __construct($mail)
{
$this->mail = $mail;
}
public function onFooAction(Event $event)
{
var_dump($event);
var_dump($event->getOrder());
echo "fooAction";
$message = \Swift_Message::newInstance()
->setSubject($event->getOrder()->getName())
->setFrom('send#example.com')
->setTo('send#example2.com)
->setBody('Test')
;
$this->mail->send($message);
}
}
all these stuff Work but I want to Understand where I have to declare the dispatcher.
I have to declare for each controller ?
$dispatcher = new EventDispatcher();
even the add listener ( or addSubscriber)
$dispatcher->addListener(StoreEvents::STORE_ORDER,array($listener,'onFooAction'),25);
I don't think this is the best practice to Declare every Time the new Dispatcher
or new addListener
UPDATE
I created another Action
/**
* #Route("/evento2", name="evento2")
*/
public function evento2Action()
{
$order = new Ordini();
$order->setName('ordine nuovo -'.rand(0,100));
$em = $this->getDoctrine()->getManager();
$em->persist($order);
$em->flush();
$event = new FilterOrderEvent($order);
$this->get('event_dispatcher')->dispatch(StoreEvents::STORE_ORDER,$event);
return $this->render('::event.html.twig');
}
And create my service:
services:
create_order_listener:
class: AppBundle\Events\StoreListener
arguments: ["#swiftmailer.mailer"]
tags:
- { name: kernel.event_listener, event: store.order, method: onFooAction
all work. But now I'd like to know the best practice or when Create listener by service and when I have to use addListener or addSubscriber
Related
I'm currently migrating a ZF2 application to ZF3.
Mostly everything is going smoothly but I'm stuck on one thing.
In my Module.php, I have an ACL management using zend-permissions-acl.
class Module
{
protected $defaultLang = 'fr';
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
if (!$e->getRequest() instanceof ConsoleRequest){
$eventManager->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'onRenderError'));
$eventManager->attach(MvcEvent::EVENT_RENDER, array($this, 'onRender'));
$eventManager->attach(MvcEvent::EVENT_FINISH, array($this, 'onFinish'));
$this->initAcl($e);
$eventManager->attach('route', array($this, 'checkAcl'));
}
}
public function checkAcl(MvcEvent $e) {
$app = $e->getApplication();
$sm = $app->getServiceManager();
$route = $e -> getRouteMatch() -> getMatchedRouteName();
$authService = $sm->get('AuthenticationService');
$jwtService = $sm->get('JwtService');
$translator = $sm->get('translator');
$identity = null;
try {
$identity = $jwtService->getIdentity($e->getRequest());
} catch(\Firebase\JWT\ExpiredException $exception) {
$response = $e->getResponse();
$response->setStatusCode(401);
return $response;
}
if(is_null($identity) && $authService->hasIdentity()) { // no header being passed on... we try to use standard validation
$authService->setJwtMode(false);
$identity = $authService->getIdentity();
}
$userRole = 'default';
$translator->setLocale($this->defaultLang);
if(!is_null($identity))
{
$userRole = $identity->getType();
//check if client or prospect
if($userRole >= User::TYPE_CLIENT)
{
$userManagementRight = UserRight::CREATE_USERS;
if($identity->hasRight($userManagementRight))
$userRole = 'userManagement';
}
$translator->setLocale($identity->getLang());
}
if (!$e->getViewModel()->acl->isAllowed($userRole, null, $route)) {
$response = $e -> getResponse();
$response->setStatusCode(403);
return $response;
}
public function initAcl(MvcEvent $e) {
//here is list of routes allowed
}
}
My issue here is that I'm still using the getServiceManager and therefore getting the deprecated warning : Usage of Zend\ServiceManager\ServiceManager::getServiceLocator is deprecated since v3.0.0;
Basically, I just need to inject dependencies into Module.php.
I guess otherwise I would have to move the checkAcl to the Controller directly and inject the ACL in them ? Not sure what is the proper way of doing this.
Any feedback on this would be greatly appreciated.
Regards,
Robert
To solve the issue you should use a Listener class and Factory. It would also help you with more separation of concerns :)
You seem quite capable of figuring stuff out, judging by your code. As such, I'm just going to give you an example of my own, so you should fill yours in with your own code (I'm also a bit lazy and do not wish to rewrite everything when I can copy/paste my code in ;) )
In your module.config.php:
'listeners' => [
// Listing class here will automatically have them "activated" as listeners
ActiveSessionListener::class,
],
'service_manager' => [
'factories' => [
// The class (might need a) Factory
ActiveSessionListener::class => ActiveSessionListenerFactory::class,
],
],
The Factory
<?php
namespace User\Factory\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use User\Listener\ActiveSessionListener;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
class ActiveSessionListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager $entityManager */
$entityManager = $container->get(EntityManager::class);
/** #var AuthenticationService $authenticationService */
$authenticationService = $container->get(AuthenticationService::class);
return new ActiveSessionListener($authenticationService, $entityManager);
}
}
The Listener
<?php
namespace User\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use User\Entity\User;
use Zend\Authentication\AuthenticationService;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Mvc\MvcEvent;
/**
* Class ActiveSessionListener
*
* #package User\Listener
*
* Purpose of this class is to make sure that the identity of an active session becomes managed by the EntityManager.
* A User Entity must be in a managed state in the event of any changes to the Entity itself or in relations to/from it.
*/
class ActiveSessionListener implements ListenerAggregateInterface
{
/**
* #var AuthenticationService
*/
protected $authenticationService;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
/**
* #var array
*/
protected $listeners = [];
/**
* CreatedByUserListener constructor.
*
* #param AuthenticationService $authenticationService
* #param ObjectManager $objectManager
*/
public function __construct(AuthenticationService $authenticationService, ObjectManager $objectManager)
{
$this->setAuthenticationService($authenticationService);
$this->setObjectManager($objectManager);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'haveDoctrineManagerUser'], 1000);
}
/**
* #param Event $event
*
* #throws \Doctrine\Common\Persistence\Mapping\MappingException
* #throws \Doctrine\ORM\ORMException
*/
public function haveDoctrineManagerUser(Event $event)
{
if ($this->getAuthenticationService()->hasIdentity()) {
// Get current unmanaged (by Doctrine) session User
$identity = $this->getAuthenticationService()->getIdentity();
// Merge back into a managed state
$this->getObjectManager()->merge($identity);
$this->getObjectManager()->clear();
// Get the now managed Entity & replace the unmanaged session User by the managed User
$this->getAuthenticationService()->getStorage()->write(
$this->getObjectManager()->find(User::class, $identity->getId())
);
}
}
/**
* #return AuthenticationService
*/
public function getAuthenticationService() : AuthenticationService
{
return $this->authenticationService;
}
/**
* #param AuthenticationService $authenticationService
*
* #return ActiveSessionListener
*/
public function setAuthenticationService(AuthenticationService $authenticationService) : ActiveSessionListener
{
$this->authenticationService = $authenticationService;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return ActiveSessionListener
*/
public function setObjectManager($objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
}
The important bits:
The Listener class must implement ListenerAggregateInterface
Must be activated in the listeners key of the module configuration
That's it really. You then have the basic building blocks for a Listener.
Apart from the attach function you could take the rest and make that into an abstract class if you'd like. Would save a few lines (read: duplicate code) with multiple Listeners.
NOTE: Above example uses the normal EventManager. With a simple change to the above code you could create "generic" listeners, by attaching them to the SharedEventManager, like so:
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(SomeClass::class, EventConstantClass::SOME_STRING_CONSTANT, [$this, 'callbackFunction']);
}
public function callbackFunction (MvcEvent $event) {...}
I'm trying to get Entity Listeners work with ODM in Symfony 2.7 but to no avail.
a51.document.listener.store:
class: A51\FilesystemBundle\EventListener\StoreEntityListener
tags:
- { name: doctrine.odm.mongodb.document_manager, event: postLoad, method: onPostLoad }
arguments: [#a51.repo.file]
and:
<?php
namespace A51\FilesystemBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use A51\FilesystemBundle\Document\Store;
use A51\FilesystemBundle\Repository\FileRepositoryInterface;
class StoreEntityListener
{
/**
* #var FileRepositoryInterface
*/
private $fileRepository;
public function __construct(FileRepositoryInterface $fileRepository)
{
$this->fileRepository = $fileRepository;
}
public function onPostLoad(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Store)
{
$entity->setTotalByteSize($this->fileRepository->findSumFilesSizeByStore($entity));
}
}
}
I've tried pretty much everything I could find in docs but for some reason onPostLoad method does not get called.
Store document gets loaded with ParamConverter:
* #ParamConverter("store", class="A51FilesystemBundle:Store")
Any help would be welcome.
I have a MongoDB listener in my project, but my code it's so different of yours. There's a simpler way, all you must to do it's import DocumentManager and then you can call it from construct to use it on all you listener. I'm gonna show you my code and tell me if this help you ;)
namespace AppBundle\OdmListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ODM\MongoDB\DocumentManager;
class RedundancyListener
{
/**
* #var DocumentManager
*/
private $dm;
/**
* #param DocumentManager $odm
*/
function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
Then inside you can do any queries or updates as you do it in you controller. Also you can use ORM or ODM CycleEvents if you import them, like I do in the example.
/**
* #param LifecycleEventArgs $eventArgs
*/
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof \AppBundle\Entity\Example) {
$subscriptionHash = $this->getSubscription($entity);
$this->dm->createQueryBuilder('AppBundle\Document\Example')
->update()
//->multiple(true)
->field('name')->set($entity->getName())
->field('last_name')->set($entity->getLastName())
->field('mail')->set($entity->getMail())
->getQuery()
->execute();
}
}
}
I need to do a custom isGranted method (not using Rbac or acl module from community). So I have a service which provides the functionality. But this code:
if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
throw new \Exception("you_are_not_allowed", 1);
}
...is duplicated in each controller and each action I have. Parameters are changing of course depends on the permission ('ZF_INV_HOM', 'ZF_TODO_DELETE' ...).
I think it's not a bad idea to do this code before the controller is called, but I can't figure what is the best solution (best architecture), and how to pass those parameters to it (I thought about annotation on controllers but how to handle this ?).
The point is, if I have to modify this code I can't imagine to do that hundreds of times, for each controllers, each action I have I need to have this code in one place.
If you don't want to pollute your Module with all this code you can also make a listener class and attach only the listener in your bootstrap method:
<?php
namespace Application\Listener;
use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;
class IsAllowedListener implements SharedListenerAggregateInterface
{
/**
* #var AuthenticationServiceInterface
*/
protected $authService;
/**
* #var UserService
*/
protected $userService;
/**
* #var \Zend\Stdlib\CallbackHandler[]
*/
protected $sharedListeners = array();
/**
* #param SharedEventManagerInterface $events
*/
public function attachShared(SharedEventManagerInterface $events)
{
$this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
}
public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
$this->authService = $authService;
$this->userService = $userService;
}
/**
* #param MvcEvent $event
*/
protected function isAllowed(MvcEvent $event)
{
$authService = $this->getAuthService();
$identity = $authService->getIdentity();
$userService = $this->getUserService();
if($userService->isGrantedCustom()){
// User is granted we can return
return;
}
// Return not allowed response
}
/**
* #return AuthenticationServiceInterface
*/
public function getAuthService()
{
return $this->authService;
}
/**
* #param AuthenticationServiceInterface $authService
*/
public function setAuthService(AuthenticationServiceInterface $authService)
{
$this->authService = $authService;
}
/**
* #return UserService
*/
public function getUserService()
{
return $this->userService;
}
/**
* #param UserService $userService
*/
public function setUserService(AuthenticationServiceInterface $userService)
{
$this->userService = $userService;
}
}
You need to setup a factory to inject your dependencies:
<?php
namespace Application\Listener;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Factory for creating the IsAllowedListener
*/
class IsAllowedListenerFactory implements FactoryInterface
{
/**
* Create the IsAllowedListener
*
* #param ServiceLocatorInterface $serviceLocator
* #return RenderLinksListener
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$userService = $serviceLocator->get('Application\Service\UserService');
return new IsAllowedListener($authService, $userService );
}
}
And register all this in config:
'service_manager' => array(
'factories' => array(
'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
)
)
And then in bootstrap:
public function onBootstrap(EventInterface $event)
{
$application = $event->getTarget();
$serviceManager = $application->getServiceManager();
$eventManager = $application->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
$sharedEventManager->attachAggregate($isAllowedListener);
}
Instead of using AbstractActionController::class, you could also make a specific class, so you will only listen to instances of that class.
So for example AbstractIsAllowedActionController::class or something like that.
By attaching an event listener to the SharedEventManager you can target all controllers and have the authorization check in just one place.
In this case the target is Zend\Mvc\Controller\AbstractActionController which means any controller extending it will execute the listener. The high priority of this listener will mean that it is executed prior to the target controller action, giving you the chance to handle any requests that have not been authorized.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach(
\Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
MvcEvent::EVENT_DISPATCH,
[$this, 'isAllowed'],
1000 // high priority
);
}
In each controller there would need to be some way that you can determine which 'resource' is being accessed.
As an example it could implement this interface
interface ResourceInterface
{
// Return a unique key representing the resource
public function getResourceId();
}
The listener could then look like this.
public function isAllowed(MvcEvent $event)
{
$serviceManager = $event->getApplication()->getServiceManager();
// We need the 'current' user identity
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$identity = $authService->getIdentity();
// The service that performs the authorization
$userService = $serviceManager->get('MyModule\Service\UserService');
// The target controller is itself a resource (the thing we want to access)
// in this example it returns an resource id so we know what we want to access
// but you could also get this 'id' from the request or config etc
$controller = $event->getTarget();
if ($controller instanceof ResourceInterface) {
$resourceName = $controller->getResourceId();
// Test the authorization, is UserX allowed resource ID Y
if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
// early exit for success
return;
} else {
// Denied; perhaps trigger a new custom event or return a response
}
}
}
Im trying to insert data into database in onKernelRequest using doctrine
//** src/MPN/CRMBundle/Listener/AnalyticsListener.php
namespace MPN\CRMBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use MPN\CRMBundle\Manager\AnalyticsManager;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManager;
use vRonn\StatisticsBundle\Entity\Statistics as Statistics;
class AnalyticsListener
{
/**
* #var AnalyticsManager
*/
private $am;
private $em;
public function __construct(AnalyticsManager $am)
{
$this->am = $am;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$uri = $request->getRequestUri();
$route = $request->get('_route');
if (!preg_match('#^/(app_dev\.php/)?(admin|js|media|_[^/]+)/?.*#', $uri)) {
// add page view count
$this->am->addPageView();
//Here is how i try to insert data
$d = new Statistics();
$d->setType(1);
$d->setName('blabla');
$d->setValue('val');
$em = $this->am->em->getRepository('vRonnStatisticsBundle:Statistics');
$em->persist($d);
$em->flush();
if (!$session->has('visitor') || $session->get('visitor') < time()) {
$session->set('visitor', time() + (24 * 60 * 60));
$this->am->addVisitor();
}
$this->am->save();
}
}
but i get error below
Undefined method 'persist'. The method name must start with either
findBy or findOneBy!
Here is the manager which i take the entity manager from, i take entity from here because i tried both $this->get('doctrine') or $this->getDoctrine() inside onKernelRequest but error
//** src/MPN/CRMBundle/Manager/AnalyticsManager.php
namespace MPN\CRMBundle\Manager;
use Doctrine\ORM\EntityManager;
use MPN\CRMBundle\Entity\Analytics;
use MPN\CRMBundle\Service\DateTimeBuilder;
class AnalyticsManager
{
/**
* #var EntityManager
*/
public $em;
/**
* #var DateTimeBuilder
*/
private $dateTimeBuilder;
/**
* #var array
*/
private $analytics;
public function __construct(EntityManager $em, DateTimeBuilder $dateTimeBuilder)
{
$this->em = $em;
$this->dateTimeBuilder = $dateTimeBuilder;
$this->setup();
}
/**
* Flushes the data to the database.
*
* #return void
*/
public function save()
{
$this->em->flush();
}
}
It looks like you're setting $em to a repository, not an entity manager. If you just do $em = $this->am->em it oughta work.
I have this entity:
<?php
namespace Comakai\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM,
Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Stuff {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Column(type="text")
* #Assert\NotBlank()
*/
private $content;
/**
* #ORM\ManyToMany(targetEntity="Apple", cascade={"persist"})
*/
private $apples;
/**
* #ORM\ManyToMany(targetEntity="Pig")
*/
private $pigs;
public function __construct() {
$this->apples = new \Doctrine\Common\Collections\ArrayCollection();
$this->pigs = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setApples($apples) {
$this->getApples()->clear();
foreach ($apples as $apple) {
$this->addApple($apple);
}
}
public function setPigs($pigs) {
$this->getPigs()->clear();
foreach ($pigs as $pig) {
$this->addPig($pig);
}
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set content
*
* #param text $content
*/
public function setContent($content) {
$this->content = $content;
}
/**
* Get content
*
* #return text
*/
public function getContent() {
return $this->content;
}
/**
* Add apples
*
* #param Comakai\MyBundle\Entity\Apple $apples
*/
public function addApple(\Comakai\MyBundle\Entity\Apple $apples) {
$this->apples[] = $apples;
}
/**
* Get apples
*
* #return Doctrine\Common\Collections\Collection
*/
public function getApples() {
return $this->apples;
}
/**
* Add pigs
*
* #param Comakai\MyBundle\Entity\Pig $pigs
*/
public function addPig(\Comakai\MyBundle\Entity\Pig $pigs) {
$this->pigs[] = $pigs;
}
/**
* Get pigs
*
* #return Doctrine\Common\Collections\Collection
*/
public function getPigs() {
return $this->pigs;
}
}
and this listener:
<?php
namespace Comakai\MyBundle\Listener;
use Comakai\MyBundle\Util\SluggerParser
Doctrine\ORM\Event\OnFlushEventArgs,
Comakai\MyBundle\Entity\Stuff,
Comakai\MyBundle\Entity\Apple,
Comakai\MyBundle\Entity\Pig;
class Listener {
/**
* #param \Doctrine\ORM\Event\OnFlushEventArgs $ea
*/
public function onFlush(OnFlushEventArgs $ea) {
$em = $ea->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() AS $entity) {
$this->save($entity, $em, $uow);
}
foreach ($uow->getScheduledEntityUpdates() AS $entity) {
$this->save($entity, $em, $uow);
}
}
public function save($entity, $em, $uow) {
if ($entity instanceof Stuff) {
$pigRepository = $em->getRepository('Comakai\MyBundle\Entity\Pig');
$content = $entity->getContent();
preg_match_all('/## pig:(\d+) ##/i', $content, $matches);
$entity->getPigs()->clear();
foreach($matches[1] as $pigID) {
$pig = $pigRepository->find($pigID);
if(!empty($pig)) {
$entity->addPig($pig);
}
}
$entity->setContent($content);
$meta = $em->getClassMetadata(get_class($entity));
$uow->recomputeSingleEntityChangeSet($meta, $entity);
$uow->computeChangeSet($meta, $entity);
}
}
}
And it works fine if apple's collection is empty, but if it has some item I get a duplication error.
How can I tell to the UnitOfWork that I only want to recalculate the pig's collection?
UPDATE
There is a new preFlush event (https://github.com/doctrine/doctrine2/pull/169) and I think this kind of things can be done there. That PR is not in the branch I'm using but let's try it!
When updating an entity during a listener's onFlush event, all you need to call is computeChangeSet():
// make changes to entity
$entity->field = 'value';
// or assign an existing entity to an assocation
$entity->user = $myExistingUserEntity;
$entity->tags->add($myExistingTagEntity);
$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);
If you're creating other entities too, you need to persist them and compute their changes first!
$myNewUserEntity = new Entity\User;
$myNewTagEntity = new Entity\Tag;
$entity->user = $myNewUserEntity;
// make sure you call add() on the owning side for *ToMany associations
$entity->tags->add($myNewTagEntity);
$em->persist($myNewUserEntity);
$em->persist($myNewTagEntity);
$metaUser = $em->getClassMetadata(get_class($myNewUserEntity));
$uow->computeChangeSet($metaUser, $myNewUserEntity);
$metaTag = $em->getClassMetadata(get_class($myNewTagEntity));
$uow->computeChangeSet($metaTag, $myNewTagEntity);
$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);
This can be done with the new preFlush event (Symfony 2.1).
Add a listener to the event (is a bad practice to inject the whole service container but sometimes is the way to go):
services:
mybundle.updater.listener:
class: Foo\MyBundle\Listener\UpdaterListener
arguments: ["#service_container"]
tags:
- { name: doctrine.event_listener, event: preFlush }
And the listener should be something like:
<?php
namespace Foo\MyBundle\Listener;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Foo\MyBundle\SomeInterface;
class UpdaterListener
{
/**
* #param \Doctrine\ORM\Event\PreFlushEventArgs $ea
*/
public function preFlush(PreFlushEventArgs $ea)
{
/* #var $em \Doctrine\ORM\EntityManager */
$em = $ea->getEntityManager();
/* #var $uow \Doctrine\ORM\UnitOfWork */
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if($entity instanceof SomeInterface) {
/*
* do your stuff here and don't worry because
* it'll execute before the flush
*/
}
}
}
}
When wanting to update the current entity you are sending to onFlush and also creating an association to that entity
(for this example I will use Parent object and child object)
Let's say when I change the parent object property 'stressed' to 1 I also want to associate a brand new child object to the parent object in my onflush method, it will look something like this:
public function onFlush(onFlushEventArgs $args)
{
....
$child = $this->createChild($em, $entity); // return the new object. just the object.
$uow->persist($child);
$childMeta = $em->getMetadataFactory()->getMetadataFor('AcmeFamilyTreeBundle:Child');
$uow->computeChangeSet($childMeta, $child)
$parent->setStressed(1);
$parentMeta = $em->getMetadataFactory()->getMetadataFor('AcmeFamilyTreeBundle:Parent');
$uow->recomputeSingleEntityChangeSet($parentMeta, $parent)
}
So there you see:
you need to persist your child object using $uow->persist() not $em->persist()
computeChangeSet on the child object.
recomputeSingleEntityChangeSet on the parent object
For help with creating the onFlush method, please see the documentation