Symfony vich uploader and doctrine loggable extension problem? - php

I am using this two libraries to create an entity that has a picture using vich/uploader-bundle and I am logging entity changes history using the loggable doctrine extension provided from stof/doctrine-extensions-bundle which provides the extension from atlantic18/doctrineextensions.
So here is the problem: I have an entity that has a Vich uploadable picture field and it is using doctrine's Gedmo loggable extension with annotations.
/**
* #var VersionedFile
*
* #ORM\Embedded(class="App\Entity\Embedded\VersionedFile")
*
* #Gedmo\Versioned()
*/
private $picture;
/**
* #var File
*
* #Vich\UploadableField(
* mapping="user_picture",
* fileNameProperty="picture.name",
* size="picture.size",
* mimeType="picture.mimeType",
* originalName="picture.originalName",
* dimensions="picture.dimensions
* )
*/
private $pictureFile;
/**
* #var DateTimeInterface
*
* #ORM\Column(type="datetime", nullable=true)
*
* #Gedmo\Versioned()
*/
private $pictureUpdatedAt;
The embedded entity class App\Entity\Embedded\VersionedFile has all the needed annotations in order to version properly using the loggable doctrine extension.
// Not the whole code but just to get the idea for property versioning
/**
* #ORM\Column(name="name", nullable=true)
*
* #Gedmo\Versioned()
*/
protected $name;
And now the problem. When I upload the file and persist the entity the following thing happens. The entity manager persist the entity and the onFlush method of the Gedmo loggable listener (Gedmo\Loggable\LoggableListener) is called. This listeners checks the changes and schedules log entries to be inserted.
The problem is that the VichUploaders upload listener (Vich\UploaderBundle\EventListener\Doctrine\UploadListener) is called after the loggable listener and then the file is uploaded which changes the properties name, size, etc. The computed changes about name, size, etc. are not available in theLoggableListener` becaues it is called first and so it doesn't know that they should be inserted.
Am I missing some configuration or am I doing something wrong. The idea is to log changes made to the picture. For now in the database the log entries consist only of the $pictureUpdatedAt field.
I debugged the problem and all I can see is the order and that in LoggableListener the method getObjectChangeSetData is returning only the $pictureUpdatedAt field that has changed. I don't think this has something in common with the Embedded entity because I think the calling order of the listeners is the problem. The first idea I had was to change the listeners priority but even if I do that the order of the calling is not changed mainly because when onFlush is called it is triggering the preUpdate method which triggers the UploadListener of the uploader bundle.

You are correct, the root of the problem is the UploadListener listens to prePersist and preUpdate while the LoggableListener listens to onFlush. Since onFlush is triggered before preUpdate, file changes are never logged. This can be fixed in a few steps.
1. Create New UploadListener
First, you can write your own UploadListener to listen to onFlush instead.
// src/EventListener/VichUploadListener.php using Flex
// src/AppBundle/EventListener/VichUploadListener.php otherwise
namespace App\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\UploadListener;
class VichUploadListener extends UploadListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
// Required if using property namer on sluggable field. Otherwise, you
// can also subscribe to "prePersist" and remove this foreach.
foreach ($uow->getScheduledEntityInsertions() as $entity) {
// We use "preUpdate" here so the changeset is recomputed.
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
}
public function getSubscribedEvents(): array
{
return [Events::onFlush];
}
}
In this example, I reuse the original UploadListener to make things easier. Since we are listening to onFlush, it is important we recompute the entity changeset after the file is uploaded which is why I used the "preUpdate" method for both scheduled updates and inserts.
You do have to be careful when changing events like this. If you have another listener that expects the value of one of your file fields to be set (or unset), this may change the expected behavior. This is especially true if you use the second foreach to handle new uploads. prePersist is triggered before onFlush, so this would make new uploads get set later than before.
2. Create New CleanListener
Next, we now have to create a new CleanListener. This listener deletes old files when we update the file field if delete_on_update is set to true. Since it listens to preUpdate, we have to change it to onFlush so old files are properly deleted.
// src/EventListener/VichCleanListener.php on Flex
// src/AppBundle/EventListener/VichCleanListener.php otherwise
namespace App\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\CleanListener;
class VichCleanListener extends CleanListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
}
public function getSubscribedEvents(): array
{
return [Events::onFlush];
}
}
3. Configure New Listeners
Now, we need to override the default listeners in our config with the ones we just wrote.
# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
# ...
vich_uploader.listener.upload.orm:
class: 'App\EventListener\VichUploadListener'
parent: 'vich_uploader.listener.doctrine.base'
autowire: false
autoconfigure: false
public: false
vich_uploader.listener.clean.orm:
class: 'App\EventListener\VichCleanListener'
parent: 'vich_uploader.listener.doctrine.base'
autowire: false
autoconfigure: false
public: false
4. Change Gedmo Extension Priorities
If all that wasn't enough, now comes the other problem you brought up: listener priority. At a minimum, we need to make sure LoggableListener is triggered after our upload/clean listeners. If you are using any of the other Gedmo extensions, you need to make sure they are loaded in the order you need them. The defaults set by VichUploaderExtension set the CleanListener to 50 and the UploadListener to 0. You can see the Gedmo Listener defaults in StofDoctrineExtensionsExtension.
For me, I have a property namer that depends on a sluggable field, so I want to make sure SluggableListener is called before the UploadListener. I also use softdeleteable and want soft deletes logged as "remove", so I want to make sure LoggableListener is registered before SoftDeleteableListener. You can change these priorities by overriding the services in your config.
# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
# ...
stof_doctrine_extensions.listener.sluggable:
class: '%stof_doctrine_extensions.listener.sluggable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: 5 }
stof_doctrine_extensions.listener.loggable:
class: '%stof_doctrine_extensions.listener.loggable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: -1 }
stof_doctrine_extensions.listener.softdeleteable:
class: '%stof_doctrine_extensions.listener.softdeleteable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: -2 }
Alternatively, you could create a compiler pass to just change the priorities of the doctrine.event_subscriber tags for these services.
// src/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php on Flex
// src/AppBundle/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php otherwise
namespace App\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class DoctrineExtensionsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$listenerPriorities = [
'sluggable' => 5,
'loggable' => -1,
'softdeleteable' => -2,
];
foreach ($listenerPriorities as $ext => $priority) {
$id = sprintf('stof_doctrine_extensions.listener.%s', $ext);
if (!$container->hasDefinition($id)) {
continue;
}
$definition = $container->getDefinition($id);
$tags = $definition->getTag('doctrine.event_subscriber');
$definition->clearTag('doctrine.event_subscriber');
foreach ($tags as $tag) {
$tag['priority'] = $priority;
$definition->addTag('doctrine.event_subscriber', $tag);
}
}
}
}
If you go this route, make sure to register the compiler pass with a higher priority (higher than 0) to ensure it is ran before RegisterEventListenersAndSubscribersPass.
// src/Kernel.php on Flex
// src/AppBundle/AppBundle.php otherwsie
// ...
use App\DependencyInjection\Compiler\DoctrineExtensionsCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new DoctrineExtensionsCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 5);
}
Now, just ensure your cache is cleared.

Related

How to pass dynamic values to container in Symfony 5

How passing a dynamic variable from controller to a service? I want manage some istance in the constructor of my service that depend by json value. My service take two parameters in the construct: a service and a variable with the JSON.
For the first one, i have passed it directly in the service.yaml. For the second one, i have some difficult.
In the controller, i get from a API the JSON. But this json it can be null.
class IndexController extends AbstractController
{
/**
* #Route("/converter-hl7", name="converter", methods={"POST"})
*/
public function index(Request $request, $myjson = null) {
$myjson = $request->getContent();
global $kernel;
$converter = $kernel->getContainer()->get('app.converter');
$xml = $converter->outputXML();
$response = new Response($xml);
$response->headers->set('Content-Type', 'xml');
return $response;
}
I stock the JSON in my .env file, MYJSON=null.
This is my service.yaml
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# explicitly configure the service
app.error:
class: App\Service\Error
public: true
autowire: true
app.converter:
class: App\Service\ConverterHl7Refacto
public: true
autowire: true
arguments:
$error: '#app.error'
$json: '%env(MYJSON)'
So, in my service called ConverterHl7Refacto.php, i have the two parameters in the constructor. I would like manage the istances if the json is empty or non. If i do a dd() of $json, i get '%env(MYJSON)' instead JSON. Why?
class ConverterHl7Refacto
{
private $ricetta;
private $identificativiDocumento;
private $codiceDocumento;
private $infoDocumento;
private $assistiti;
private $partecipanti;
private $relatedDoc;
private $structuredBody;
private $root;
private $error;
public function __construct(string $json,Error $error) {
$this->error = $error;
if ($json){
$this->ricetta = new Ricetta(json_decode($json));
$this->root = new Root();
$this->identificativiDocumento = new Identificativi();
$this->codiceDocumento = new CodiceDocumento($this->ricetta);
$this->infoDocumento = new InfoDocumento($this->ricetta);
$this->assistiti = new Assistiti($this->ricetta);
$this->partecipanti = new Partecipanti($this->ricetta);
$this->relatedDoc = new RelatedDocument($this->ricetta);
$this->structuredBody = new StructuredBody($this->ricetta);
}
}
Nothing particularly tricky about the factory concept. A factory is basically used to create an instance of a given type. I did not test the following code so apologies for typos:
class Error {}
class Converter {
public function __construct(string $json, Error $error)
{
// Whatever
...
class ConverterFactory {
private $error;
public function __construct(Error $error) {
$this->error = $error;
}
public function create(string $json) : Converter {
return new Converter($json,$this->error);
}
}
class MyController {
public function action(ConverterFactory $converterFactory)
{
$json = $request->getContent();
$converter = $converterFactory->create($json);
You will need to exclude your Converter class from autowire in your services.yaml file. But that should be all you need. No need to explicitly define things like app.converter as autowire will take care of all that.

symfony 5 - event not being dispatched

I use an event subscriber to handle some actions when my order form is submitted.
Problem my event is not being dispached but symfony is able to find him because he tells me that my OrderEvent::ORDER_CREATE is orphan.
I excpected that execution was stopped with die('Hello you from subscriber'); but it's not.
Controller
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, EventDispatcherInterface $eventDispatcher)
{
// DO PREVIOUS STUFF
$Order = new Order();
$OrderForm = $this->createForm(OrderForm::class, $Order);
if ($request->isMethod('POST')) {
$OrderForm->handleRequest($request);
if ($OrderForm->isSubmitted() && $OrderForm->isValid()) {
// OrderForm is valid proceed
$eventDispatcher->dispatch(
new OrderEvent($Order),
OrderEvent::ORDER_CREATE
);
}
}
OrderEvent
<?php
namespace App\Event;
use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;
class OrderEvent extends Event
{
public const ORDER_CREATE = 'order.created';
protected $order;
public function __construct(Order $order)
{
$this->order= $order;
}
public function getOrder(): Order
{
return $this->order;
}
}
OrderSubscriber
<?php
namespace App\EventSubscriber;
use App\Event\CommandeWebEvent;
use App\Service\SelectionWeb\SelectionWeb;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderSubscriber implements EventSubscriberInterface
{
private $entityManager;
private $selectionWeb;
public function __construct(EntityManagerInterface $entityManager, SelectionWeb $selectionWeb)
{
$this->entityManager = $entityManager;
$this->selectionWeb = $selectionWeb;
}
public static function getSubscribedEvents()
{
return [
OrderEvent::ORDER_CREATE => [
// The higher the number, the earlier the method is called.
['processOrder', 10],
['notifyOrder', -10]
]
];
}
public function processOrder(OrderEvent $event)
{
// TODO
die('Hello you from subscriber');
}
public function notifyOrder(OrderEvent $event)
{
// TODO
}
}
EDIT
The only workaround found (thx to #nikserg) is to inject subscriber into controller action (subscriber has dependencies) then register my subscriber as service in services.yaml finaly use $eventDispatcher->addSubscriber($subscriber); before $eventDispatcher->dispatch(new OrderEvent($Order),OrderEvent::ORDER_CREATE);
It seems all that stuff is really complex for a task as simple as that
EDIT2
I found an another way I'm able to execute my subscriber without usage of $eventDispatcher->addSubscriber($subscriber); and only with $eventDispatcher->dispatch(new OrderEvent($Order)); only if I configure my subscriber as service in services.yaml but why symfony does need this information in services.yaml ? Thought that everything in src/ is avaible to be used as service..
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# If I add those It works
App\EventSubscriber\OrderSubscriber:
autowire: true
EDIT3
My OrderSubscriber is loaded into container so why I should set it explicitly to being execute ? I can't figure out what's going on
php bin/console debug:container
---------------------------------------- ---------------------------------------
Service ID Class name
---------------------------------------- ----------------------------------------
App\EventSubscriber\OrderSuscriber App\EventSubscriber\OrderSuscriber
EDIT 4
If I set my OrderSubscriber explicitly there is two instances of it into container.
Why symfony execute one set explicitly and not the one set with resource: '../src/*'
Symfony will autowire your subscriber as service, if you will require it as argument in action:
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, OrderSubscriber $orderSubscriber)
Of course, if your subscriber is registered properly.
But let me advice you not to create subscribers as objects manually. The main good thing about subscribers is that you know nothing about them, when you fire event. There could be dozens of subscribers to this event, and all of them will proceed your event. That will keep your code nice and lower cohesion.
It's in docs: https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber
First I want to thank you all for your time and let me apologize my problem was due to a typo I wrote OrderSuscriber instead of OrderSubscriber that's why there was 2 services into my container and why defined service explicitly was working.

Saving the getEntityChangeSet() result in Doctrine EventListener

I need to create changelog in the API for user actions on entities.
For example:
User updates entity Licensor I need to catch the changes and save them in the database in different table.
The first part I was able to do with Doctrine Event Listener
class ChangelogEventListener
{
public function preUpdate($obj, PreUpdateEventArgs $eventArgs)
{
if ($obj instanceof LoggableInterface) {
dump($eventArgs->getEntityChangeSet());
}
}
}
And with marking entity event listeners
/**
* #ORM\EntityListeners(value={"AppBundle\EventSubscriber\Changelogger\ChangelogEventListener"})
*/
class Licensor implements LoggableInterface
But I'm not sure if it's even possible and if it makes sense to access the ORM entity manager in a preUpdate event.
If it isn't then what's the proper way to do it?
I've tried with Symfony's EventListener instead of Doctrine's but then I don't have access to getEntityChangeSet().
Check out Doctrine events, and specifically the preUpdate event. This event is the most restrictive, but you do have access to all of the fields that have changed, and their old/new values. You can change the values here on the entity being updated, unless it's an associated entity.
Check out this answer, which suggests using an event subscriber, and then persisting to a logging entity.
There is also this blog post that uses the preUpdate event to save a bunch of changesets to the internal listener class, then postFlush it persists any entities that are being changed, and calls flush again. However, I would not recommend this, as the Doctrine documentation explicitly states:
postFlush is called at the end of EntityManager#flush().
EntityManager#flush() can NOT be called safely inside its listeners.
If you went the route of that blog post you'd be better off using the onFlush() event and then doing your computeChangeSets() call after your persist(), like the first answer I posted.
You can find a similar example here:
You are better off using an event listener for such thing. What you want is more like a database trigger to log changes. See example below (tested and works fine) which logs User entity changes in UserAudit entity. For demonstration purposes, it only watches username and password field but you can modify it as you wish.
Note: If you want an entity listener then look at this example.
services.yml
services:
application_backend.event_listener.user_entity_audit:
class: Application\BackendBundle\EventListener\UserEntityAuditListener
arguments: [ #security.context ]
tags:
- { name: doctrine.event_listener, event: preUpdate }
- { name: doctrine.event_listener, event: postFlush }
UserEntityAuditListener
namespace Application\BackendBundle\EventListener;
use Application\BackendBundle\Entity\User;
use Application\BackendBundle\Entity\UserAudit;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Symfony\Component\Security\Core\SecurityContextInterface;
class UserEntityAuditListener
{
private $securityContext;
private $fields = ['username', 'password'];
private $audit = [];
public function __construct(SecurityContextInterface $securityContextInterface)
{
$this->securityContext = $securityContextInterface;
}
public function preUpdate(PreUpdateEventArgs $args) // OR LifecycleEventArgs
{
$entity = $args->getEntity();
if ($entity instanceof User) {
foreach ($this->fields as $field) {
if ($args->getOldValue($field) != $args->getNewValue($field)) {
$audit = new UserAudit();
$audit->setField($field);
$audit->setOld($args->getOldValue($field));
$audit->setNew($args->getNewValue($field));
$audit->setUser($this->securityContext->getToken()->getUsername());
$this->audit[] = $audit;
}
}
}
}
public function postFlush(PostFlushEventArgs $args)
{
if (! empty($this->audit)) {
$em = $args->getEntityManager();
foreach ($this->audit as $audit) {
$em->persist($audit);
}
$this->audit = [];
$em->flush();
}
}
}

How to add an entity-specific listener in Symfony2 that has access to container

In a Symfony2 application, I have an entity that needs to be populated on pre-persist with various context properties (like user id, what page it was called from, etc.)
I figured that to do this, I need to add a doctrine event listener that has access to "service_container", and the best way to give such access is to pass "service_container" as an argument to this listener.
I have a specific entity that I want to listen to, and I do not want to trigger the listener to events with any other entity.
We can add an entity-specific listener, documentation is found here:
http://docs.doctrine-project.org/en/latest/reference/events.html#entity-listeners
- but this does not provide example of how to pass an argument (I use PHP annotations to declare the listener).
I also tried to use JMSDiExtraBundle annotations, like in the example below:
http://jmsyst.com/bundles/JMSDiExtraBundle/master/annotations#doctrinelistener-or-doctrinemongodblistener
- but this way requires to declare the listener as non-entity-specific
Is there any way to make a listener for one entity only, and have it have access to container?
One of the ways similar to doctrine docs through dependency injection:
<?php
namespace AppBundle\EntityListener;
use AppBundle\Entity\User;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouterInterface;
class UserListener {
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function postPersist(User $user, LifecycleEventArgs $args)
{
$logger = $this->logger;
$logger->info('Event triggered');
//Do something
}
}
services:
user.listener:
class: AppBundle\EntityListener\UserListener
arguments: [#logger]
tags:
- { name: doctrine.orm.entity_listener }
And dont forget add listener to entity mapping:
AppBundle\Entity\User:
type: entity
table: null
repositoryClass: AppBundle\Entity\UserRepository
entityListeners:
AppBundle\EntityListener\UserListener: ~
I would simply check entity type from the event. If you check type inside or outside the subscriber, it has the same performance cost. And simple type condition is fast enough.
namespace App\Modules\CoreModule\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class SetCountryToTaxSubscriber implements EventSubscriber
{
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [Events::prePersist];
}
public function prePersist(LifecycleEventArgs $lifecycleEventArgs)
{
$entity = $lifecycleEventArgs->getEntity();
if ( ! $entity instanceof Tax) {
return;
}
$entity->setCountry('myCountry');
}
}

Symfony 2.1 - Custom Json Annotation Listener

For a Symfony 2.1 project, I'm trying to create a new annotation #Json() that will register a listener that will create the JsonResponse object automatically when I return an array. I've got it working, but for some reason the listener is always called, even on methods that don't have the #Json annotation. I'm assuming my approach works, since the Sensio extra bundle does this with the #Template annotation.
Here is my annotation code.
<?php
namespace Company\Bundle\Annotations;
/**
* #Annotation
*/
class Json extends \Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationAnnotation
{
public function getAliasName()
{
return 'json';
}
}
Here is my listener code.
<?php
namespace Company\Bundle\Listener\Response\Json;
class JsonListener
{
//..
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
$data = $event->getControllerResult();
if(is_array($data) || is_object($data)) {
if ($request->attributes->get('_json')) {
$event->setResponse(new JsonResponse($data));
}
}
}
}
This is my yaml definition for the listener.
json.listener:
class: Company\Bundle\Listener\Response\Json
arguments: [#service_container]
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
I'm obviously missing something here because its being registered as a kernel.view listener. How do I change this so that it is only called when a #Json() annotation is present on the controller action?
Not pretend to be the definitive answer.
I'm not sure why your are extending ConfigurationAnnotation: its constructor accepts an array, but you don't need any configuration for your annotation. Instead, implement ConfigurationInterface:
namespace Company\Bundle\Annotations;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface;
/**
* #Annotation
*/
class Json implements ConfigurationInterface
{
public function getAliasName()
{
return 'json';
}
public function allowArray()
{
return false;
}
}
Sensio ControllerListener from SensionFrameworkExtraBundle will read your annotation (merging class with methods annotations) and perform this check:
if ($configuration instanceof ConfigurationInterface) {
if ($configuration->allowArray()) {
$configurations['_'.$configuration->getAliasName()][] = $configuration;
} else {
$configurations['_'.$configuration->getAliasName()] = $configuration;
}
}
Setting a request attribute prefixed with _. You are correctly checking for _json, so it should work. Try dumping $request->attributes in your view event listener. Be sure that your json.listener service is correctly loaded too (dump them with php app/console container:debug >> container.txt).
If it doesn't work, try adding some debug and print statements here (find ControllerListener.php in your vendor folder):
var_dump(array_keys($configurations)); // Should contain _json
Remember to make a copy of it before edits, otherwise Composer will throw and error when updating dependencies.

Categories