OnDispatch event in Service Constructor - php

I have problem with attaching OnDispatch event in Apigility. I want to have value from custom header in constructor of my abstract Service Class. It works when I simply add it to on bootstrap in Module.php
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$eventManager->attach(\Zend\Mvc\MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'));
}
public function onDispatch(\Zend\Mvc\MvcEvent $e){
$e->getRequest()->getHeaders()->get('User-Token')->getFieldValue();
}
But I don't know how to pass this value to ServiceAbstract Constructor.
I also tried with implementing EventManagerAwareInterface and attaching events with attachDefaultListeners() method but that didn't get me any results - attached function was not called. What is the proper approach to attaching to events NOT in controllers? Thanks for any help.

It seems to me that you have two different goals here.
1. Store a header variable from a route event.
2. Get a variable into the constructor function of a class
To answer 1.
You can make a listener class and attach this class to your eventManager. This would look something like this:
<?php
namespace My\Listener;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;
use Zend\Http\Headers;
use Zend\Http\Request as HttpRequest;
class MyCustomListener implements ListenerAggregateInterface
{
/**
* #var \Zend\Stdlib\CallbackHandler[]
*/
protected $listeners = array();
/**
* #param EventManagerInterface $eventManager
*/
public function attach(EventManagerInterface $eventManager)
{
// attach on route
$this->listeners[] = $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'));
}
/**
* #param EventManagerInterface $eventManager
*/
public function detach(EventManagerInterface $eventManager)
{
foreach ($this->listeners as $index => $listener) {
if ($eventManager->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* Do your thing on dispatch event with your headers
*
* #param MvcEvent $event
*/
public function onDispatch(MvcEvent $event)
{
$request = $event->getRequest();
if(!$request instanceof HttpRequest){
// Nothing to do
return;
}
$headers = $request->getHeaders();
// You could for example get a service here and store your value
}
}
You attach this listener in Module.php like this:
public function onBootstrap(MvcEvent $event)
{
$event->getApplication();
$application->getEventManager();
$eventManager->attach($serviceManager->get('My\Listener\MyCustomListener'));
}
You have to register your listener in your ServiceManager either under invokables or factories with the key My\Listener\MyCustomListener to be able to attach it here like this.
To answer 2:
To get a variable in your constructor you can make a factory for your class and get the variable from the service that holds the variable that you need (could be from the listener from 1 directly).
<?php
namespace My\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use My\Folder\MyCustomClass;
class MyClassFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return Logger
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$controllerPluginManager = $serviceLocator;
$service = $serviceManager->get(My\Service\MyStorageService);
$dependency = $service->getDependency();
return new MyCustomClass($dependency);
}
}

Related

How to inherit custom Doctrine class annotation in Symfony?

I have created a custom #TimestampAware annotation which can be used to automatically update a timestamp property when persisting or updating my entities (see code below).
When using this annotation directly on an entity class everything works fine. However, when using the annotation on a base class, it is not recognized on the inherited sub-classes.
/**
* #TimestampAware
*/
class SomeEntity { ... } // works fine
/**
* #TimestampAware
*/
class BaseEntity { ... }
class SubEntity extends BaseEntity { ... } // Annotation is not recognized
Is it the intended behavior of Doctrine annotations and the Annotation Reader class to only look for annotation directly on the current class and no include its parent classes? Or is there something wrong with my implementation?
My annotation:
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Reader;
/**
* #Annotation
* #Target("CLASS")
*/
final class TimestampAware { }
The annotation listener:
use Doctrine\Common\EventSubscriber;
class TimestampAwareSubscriber implements EventSubscriber {
protected $reader;
protected $logger;
public function __construct(Reader $reader, LoggerInterface $logger) {
$this->reader = $reader;
$this->logger = $logger;
}
public function getSubscribedEvents() {
return [
Events::prePersist,
Events::preUpdate,
];
}
public function prePersist(LifecycleEventArgs $args) {
$this->onPersistOrUpdate($args);
}
public function preUpdate(LifecycleEventArgs $args) {
$this->onPersistOrUpdate($args);
}
protected function onPersistOrUpdate(LifecycleEventArgs $args) {
$this->logger->info("Reader: ".get_class($this->reader));
$entity = $args->getEntity();
$reflection = new \ReflectionClass($entity);
$timestampAware = $this->reader->getClassAnnotation(
$reflection,
TimestampAware::class
);
if (!$timestampAware) {
return;
}
// update timestamp...
}
}
The Annotation Reader inspects only the relevant class, it does not read the annotations of the parent class. This can easily be checked with code like this:
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
AnnotationRegistry::registerLoader('class_exists');
/**
* #Annotation
* #Target("CLASS")
*/
class Annotated {}
/**
* #Annotated
**/
class ParentFoo {}
class ChildFoo extends ParentFoo {}
$reader = new AnnotationReader();
$parentAnnotated = $reader->getClassAnnotation(
new ReflectionClass(ParentFoo::class),
Annotated::class
);
var_dump($parentAnnotated);
// Outputs `object(Annotated)#10 (0) {}`
$childAnnotated = $reader->getClassAnnotation(
new ReflectionClass(ChildFoo::class),
Annotated::class
);
var_dump($childAnnotated);
// outputs `null`
If you want to check parent classes, you'll have to do it yourself. ReflectionClass provides the getParentClass() method which you could do to check the class hierarchy.
Tangentially, I've found this package that claims to extend annotations so that they are usable with inheritance directly. Haven't checked if it's any good.
In addition to the great answer by #yivi I would like to share the code I used to solve the problem. Maybe this helps others how encounter the same problem:
protected function onPersistOrUpdate(LifecycleEventArgs $args) {
$this->logger->info("Reader: ".get_class($this->reader));
$entity = $args->getEntity();
$reflection = new \ReflectionClass($entity);
$timestampAware = $this->reader->getClassAnnotation(
$reflection,
TimestampAware::class
);
while (!$timestampAware && $reflection = $reflection->getParentClass()) {
$timestampAware = $this->reader->getClassAnnotation(
$reflection,
TimestampLogAware::class
);
}
if (!$timestampAware) {
return;
}
// update timestamp...
}

Prevent a listener from being activated depending on where it was called in Symfony

I have a Symfony project with a lot of bundles.
In one of them, I have a standard Doctrine listener like this:
class MyListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($entity instanceof MyEntity) {
//do something
}
...
Now I've created a new Bundle that also loads these Entities in a Controller.
As expected, it also triggers the postLoad in the listener.
I need it not to trigger it, or if it's triggered by this Bundle/Controller, to don't do anything, something like:
class MyListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($caller = "DontTriggerBundle")
return true;
}
if ($entity instanceof MyEntity) {
//do something
}
...
Is there a way to do this? Thanks in advance
So this is how I solved it:
Added the request stack to the service:
<service id="myservice>
<argument type="service" id="request_stack"/>
</service>
Then got the controller like this:
// src/AppBundle/EventListener/AcmeListener.php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class AcmeListener
{
/** #var Request */
protected $request;
/**
* AcmeListener constructor.
*
* #param RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->request = $requestStack->getCurrentRequest();
}
/**
* #param LifecycleEventArgs $args
*/
public function postLoad(LifecycleEventArgs $args)
{
$controller = $this->request->attributes->get('_controller');
if (strpos($controller, 'DontTriggerController::indexAction') !== false) {
// Do nothing
return;
}
// Do somethings
}
}
hope this helps someone

Symfony EventSubscribe on Entity

trying to make an subscriber for Entity actions (CRUD) and cannot figure it out.
I know there is a way, where I can make listener and send him 3 different events, but that's not what I want to reach, I dont even think is good solution.
Event Subscriber
<?php
namespace App\EventListener;
use App\Entity\Log;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Part of program created by David Jungman
* #author David Jungman <davidjungman.web#gmail.com>
*/
class EntitySubscriber implements EventSubscriberInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, EntityManagerInterface $em)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return array(
Events::postPersist,
Events::postUpdate,
Events::postRemove,
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->logEvent($args, "remove");
}
public function postRemove(LifecycleEventArgs $args)
{
$this->logEvent($args, "remove");
}
public function postPersist(LifecycleEventArgs $args)
{
$this->logEvent($args, "create");
}
private function logEvent(LifecycleEventArgs $args, string $method)
{
$entity = $args->getEntity();
if($entity->getShortName() != "Log")
{
$user = $this->tokenStorage->getToken()->getUser();
$log = new Log();
$log
->setUser($user)
->setAffectedTable($entity->getShortName())
->setAffectedItem($entity->getId())
->setAction($method)
->setCreatedAt();
$this->em->persist($log);
$this->em->flush();
}
}
}
and my Service.yaml part
App\EventListener\EntitySubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
I have tried:
I've looked into these 2 official tutorials:
-https://symfony.com/doc/current/event_dispatcher.html
-https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
but neither helped.. when I use shown part of config, my computer freeze.
When I try to debug it, I can see these methods active
( php bin/console debug:event-dispatcher )
but they are listening on "event" event
Doctrine has it's own events handler/subscriber system. However, with the class Symfony\Component\EventDispatcher\EventSubscriberInterface; that you are implementing, that is from the Symfony event system.
<?php
use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber; // **the Doctrine Event subscriber interface**
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class MyEventSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
Events::postUpdate,
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product) {
// do something with the Product
}
}
}

Must I move data dependency out of my Controllers (and into Factories)?

This question can be viewed through a prism of ZF2 + Doctrine + MVC programming practices, or it can be viewed through just an OOP perspective.
My concern is about Separation of Concerns, and on removing dependencies.
I am using code in my controllers that goes something like this:
class MyController
{
private $em; //entityManager
function __construct()
{
$this->em = DoctrineConnector::getEntityManager();
}
function indexAction()
{
//Input
$inputParameter = filter_input(...);
//request for Data
$queryBuilder = $this->em->createQuery(...)
->setParameter('param', $inputParameter);
$query = $queryBuilder->getQuery();
//$services is the user-defined data type requested
$services = $query->getResult();
//use data to produce a view model
$view = new ViewModel();
$view->setVariables(array('services' => $services));
return $view;
}
}
I am not entirely comfortable with the above and wanted a second opinion. For one, my EntityManager is part of the class, so my class is cognizant of the entity manager construct, when I think it should not be a part of the controller. Do I perhaps use a Factory or Builder design pattern to help me create MyController class?
If I do, I can move my em (entityManager) construct into the Factory pattern and create and populate my MyController inside the Factory. Then, the MyController can have a private variable $services instead.
i.e.
class MyController
{
private $services;
function setServices($services)
{
$this->services = $services;
}
function indexAction()
{
//use data to produce a view model
$view = new ViewModel();
$view->setVariables(array('services' => $this->services));
return $view;
}
}
class MyFactoryMethod
{
function createMyController()
{
//Input
$inputParameter = filter_input(INPUT_GET...);
//request for Data
$queryBuilder = $this->em->createQuery(...)
->setParameter('param', $inputParameter);
$query = $queryBuilder->getQuery();
//$services is the user-defined data type requested
$services = $query->getResult();
//create and return MyController instance
$controller = new MyController();
$controller->setServices($services);
return $controller;
}
}
I typically tried to do this PHP's mysql extension to remove dependency on data out of my various objects. I am using Doctrine2 now which is an ORM, and wondering if I should keep doing the same thing (namely preferring 2nd example rather than the first...
Question:
I can write code both ways. It works essentially the same. My question is -- is the code, as it is written in my 2nd example preferred more than the code as it is written in my first?
Notes / Clarifications:
In my case variable $services is a domain-specific variable (not ZF2's ServiceLocator). i.e. think of MyController as a controller for business-specific "services".
I am not harnessing full power of ZF2 with configs, routers, events, and everything. I am using ZF2 modules on an existing legacy codebase on as-needed basis.
When your controller has hard dependencies I would suggest to use the common ZF2 solution by creating the controller and injecting the dependency in a factory instance and registering the controller under the 'factories' key in your 'controllers' config array.
In your module.config.php
'controllers' => array(
'factories' => array(
'Application\Controller\MyController' => 'Application\Controller\MyControllerFactory'
)
)
In your controller I would set hard dependency in the __construct method. Like this you prevent the controller from ever being instantiated without your dependencies (it will throw an exception).
Never inject something like $services (if this is a ServiceLocator) from which you will pull the actual dependencies since it is not clear what the class actually needs. It will be harder to understand for other developers and it is also hard to test since you cannot set mocks for your individual dependencies so easily.
Your Controller class:
<?php
namespace Application\Controller;
use Doctrine\ORM\EntityManager;
class MyController
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
*
*/
function indexAction()
{
//Do stuff
$entityManager = $this->getEntityManager();
}
/**
* #return EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
}
Your Factory:
<?php
namespace Application\Controller;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Doctrine\ORM\EntityManager;
class MyControllerFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return MyController
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** #var EntityManager $entityManager */
$serviceManager = $serviceLocator->getServiceLocator()
$entityManager = $serviceManager->get('doctrine.entitymanager.orm_default');
$myController = new MyController($entityManager);
return $myController;
}
}
There are two different approaches to this problem that are provided by ZF2.
Use the ServiceLocator to retrieve the EntityManager via a Factory.
In Module.php, add an anonymous function or Factory.
public function getServiceConfig()
{
return [
'factories' => [
'Doctrine\ORM\EntityManager' => function (ServiceManager $sm) {
$entityManager = $sm->get('doctrine.entitymanager.orm_default');
return $entityManager;
}
],
],
}
In your Controller
$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
Create an Initializer and AwareInterface to inject the EntityManger into your controllers.
The AwareInterface can be added to any class which is initialized by the ServiceManager.
interface EntityManagerAwareInterface
{
/**
* Set EntityManager locator
*
* #param EntityManager $entityManager
*/
public function setEntityManager(EntityManager $entityManager);
/**
* Get service locator
*
* #return EntityManager
*/
public function getServiceLocator();
}
The Initializer is run when services are initialized by the ServiceManager. A check is performed to so if $instance is a EntityManagerAwareInterface.
use Application\EntityManager\EntityManagerAwareInterface;
use Zend\ServiceManager\InitializerInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class EntityManagerInitializer implements InitializerInterface
{
/**
* Initialize
*
* #param $instance
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function initialize($instance, ServiceLocatorInterface $serviceLocator)
{
if ($instance instanceof EntityManagerAwareInterface) {
$entityManager = $serviceLocator->get('doctrine.entitymanager.orm_default');
$instance->setEntityManager($entityManager);
}
}
}
Next add the Initializer to Module.php
public function getServiceConfig()
{
return [
'initializers' => [
'entityManager' => new EntityManagerInitializer(),
],
],
}
The advantage of going the Initializer route is there is a one time setup. Any class that implements the EntityManagerAwareInterface will have the EntityManager injected when the class is initialized.

Disable Zend Developer Tools

How can I disable ZF2 developer tools in a particular controller?
I've already tried returning an terminal ViewModel, but it still renders.
You could create your own listener that fires before the zdt logic that detaches the events based on a specific controller.
<?php
namespace Application\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\Mvc\MvcEvent;
use Zend\ServiceManager\ServiceLocatorInterface;
class DetachZdtListener extends AbstractListenerAggregate
{
protected $listeners = array();
protected $serviceLocator;
public function __construct(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function attach(\Zend\EventManager\EventManagerInterface $events)
{
// Attach a listener to the finish event that has a priority sooner
// than the ZDT listener(s)
$this->listeners[] = $events->attach(MvcEvent::EVENT_FINISH,
array($this, 'onFinish'), -9499
);
}
/**
* The method called when event is fired
*
* #param \Zend\Mvc\MvcEvent $e
*/
public function onFinish(MvcEvent $e) {
$controller = $e->getController();
if ($controller === 'Application\Controller\SomeController') {
$sm = $this->serviceLocator;
$eventManager = $e->getApplication()->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$eventManager->detachAggregate($sm->get('ZendDeveloperTools\FlushListener'));
$eventManager->detachAggregate($sm->get('ZendDeveloperTools\ProfilerListener'));
$sharedEventManager->clearListeners('profiler');
}
}
}
Then you would just need to attach this listener in the onBootstrap method of one fo your modules, and it should do what you're looking for.

Categories