Symfony2 event subscriber does not call listeners - symfony

I am trying to set up a simple event subscription based on the example given here - http://symfony.com/doc/master/components/event_dispatcher/introduction.html.
Here's my event store:
namespace CookBook\InheritanceBundle\Event;
final class EventStore
{
const EVENT_SAMPLE = 'event.sample';
}
Here's my event subscriber:
namespace CookBook\InheritanceBundle\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;
class Subscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
var_dump('here');
return array(
'event.sample' => array(
array('sampleMethod1', 10),
array('sampleMethod2', 5)
));
}
public function sampleMethod1(Event $event)
{
var_dump('Method 1');
}
public function sampleMethod2(Event $event)
{
var_dump('Method 2');
}
}
Here's the config in services.yml:
kernel.subscriber.subscriber:
class: CookBook\InheritanceBundle\Event\Subscriber
tags:
- {name:kernel.event_subscriber}
And here's how I raise the event:
use Symfony\Component\EventDispatcher\EventDispatcher;
use CookBook\InheritanceBundle\Event\EventStore;
$dispatcher = new EventDispatcher();
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);
Expected output:
string 'here' (length=4)
string 'Method 1' (length=8)
string 'Method 2' (length=8)
Actual output:
string 'here' (length=4)
For some reason, the listener methods don't get called. Anyone knows what's wrong with this code? Thanks.

What #Tristan said. The tags portion in your services file is part of the Symfony Bundle and is only processed if you pull the dispatcher out of the container.
Your example will work as expected if you do this:
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new Subscriber());
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);

You might try to inject a configured EventDispatcher (#event_dispatcher) instead of instanciating a new one (new EventDispatcher)
If you only create it and add an event-listener Symfony still has no reference to this newly created EventDispatcher object and won't use it.
If you are inside a controller who extends ContainerAware :
use Symfony\Component\EventDispatcher\EventDispatcher;
use CookBook\InheritanceBundle\Event\EventStore;
...
$dispatcher = $this->getContainer()->get('event_dispatcher');
$dispatcher->dispatch(EventStore::EVENT_SAMPLE);
I've adapted my answer thanks to this question's answer even though the context of both questions are different, the answer still applies.

Related

How to deserialize DateTime in this case?

I can't for the life of me figure out how to handle a regular deserialization. I've read dozens of SO questions and also the official doc and it seems to be easy. Seems.
I've got a simple JSON, like:
[{"id":"00112063002463454431","first_name":"John","last_name":"Doe","date_of_birth":"2006-09-28"}]
Now I'd like to map it to my class Person. No matter what I've tried, it always complains about date_of_birth to be string. It is expected to be DateTimeInterface when the routine internally calls setDateOfBirth(?DateTimeInterface $dateOfBirth) inside the Person class. But in my understanding DateTimeNormalizer's denormalize() should've already converted it to a DateTime object before it hydrates the Person object, shouldn't it?
Inside my class the field is defined as follows:
#[ORM\Column(type: Types::DATE_MUTABLE)]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
Deserializing process:
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer(), new ArrayDenormalizer()],
[new JsonEncoder()]
);
$personsFromJson = $serializer->deserialize($requestContent, 'App\Entity\Person[]', 'json');
Is there anything else to do?!
Edit
One-class example:
<?php
namespace App\Controller;
use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
class TestController extends AbstractController
{
#[Route(path: '/test', name: 'app_test')]
public function index(): Response
{
$json = '{"dateOfBirth":"2023-01-31"}';
$serializer = new Serializer(
[new DateTimeNormalizer(), new GetSetMethodNormalizer()],
[new JsonEncoder()]
);
$testPersonFromJson = $serializer->deserialize($json, TestPerson::class, 'json');
return $this->json($testPersonFromJson);
}
}
class TestPerson {
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeInterface $dateOfBirth = null;
public function getDateOfBirth(): ?DateTimeInterface {
return $this->dateOfBirth;
}
public function setDateOfBirth(?DateTimeInterface $dateOfBirth): self {
$this->dateOfBirth = $dateOfBirth;
return $this;
}
}
App\Controller\TestPerson::setDateOfBirth(): Argument #1
($dateOfBirth) must be of type ?DateTimeInterface, string given,
called in
[...]\vendor\symfony\serializer\Normalizer\GetSetMethodNormalizer.php
on line 163
The DateTimeNormalizer() gets instantiated, but indeed, denormalize() gets never called.
I had to add a ReflectionExtractor to my ObjectNormalizer. I didn't notice this anywhere.
new ObjectNormalizer(null, null, null, new ReflectionExtractor())
https://symfony.com/doc/current/components/property_info.html#reflectionextractor
Found via the comments after my 18567th search at: Symfony Serialize Entity with Datetime / Deserialize fails

Silex + Doctrine2 ORM + Dropdown (EntityType)

I have a controller that renders a form that is suppose to have a dropdown with titles mapped against a client_user entity. Below is code I use in my controller to create the form:
$builder = $this->get(form.factory);
$em = $this->get('doctrine.entity_manager');
$form = $builder->createBuilder(new ClientUserType($em), new ClientUser())->getForm();
Below is my ClientUserType class with a constructor that I pass the entity manager on:
<?php
namespace Application\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class ClientUserType extends AbstractType
{
protected $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', EntityType::class, array(
'class' => '\\Application\\Model\\Entity\\Title',
'em' => $this->entityManager
))
->add('name')
->add('surname')
->add('contact')
->add('email');
}
public function getName()
{
return 'client_user_form';
}
}
I keep on getting this catchable fatal error below and have no idea what I need to do in order to get a dropdown with titles from a database with doctrine.
Catchable fatal error: Argument 1 passed to Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must be an instance of Doctrine\Common\Persistence\ManagerRegistry, none given, called in D:\web\playground-solutions\vendor\symfony\form\FormRegistry.php on line 90 and defined in D:\web\playground-solutions\vendor\symfony\doctrine-bridge\Form\Type\DoctrineType.php on line 111
Reading from that error I have no idea where I need to create a new instance of ManagerRegistry registry as it appears that the entity manager does not work. I am also thinking perhaps I need to get the ManagerRegistry straight from the entity manager itself.
Can someone please help explain the simplest way to get this to work? What could I be missing?
Seems that doctrine-bridge form component is not configured.
Add class
namespace Your\Namespace;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
class ManagerRegistry extends AbstractManagerRegistry
{
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container;
}
}
and configure doctrine-bridge form component
$application->register(new Silex\Provider\FormServiceProvider(), []);
$application->extend('form.extensions', function($extensions, $application) {
if (isset($application['form.doctrine.bridge.included'])) return $extensions;
$application['form.doctrine.bridge.included'] = 1;
$mr = new Your\Namespace\ManagerRegistry(
null, array(), array('em'), null, null, '\\Doctrine\\ORM\\Proxy\\Proxy'
);
$mr->setContainer($application);
$extensions[] = new \Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($mr);
return $extensions;
});
array('em') - em is key for entity manager in $application
For others that may find this: If you want to use the EntityType and you're not using a framework at all, you need to add the DoctrineOrmExtension to your FormFactoryBuilder like so:
$managerRegistry = new myManagerRegistry(
'myManager',
array('connection'),
array('em'),
'connection',
'em',
\Doctrine\ORM\Proxy\Proxy::class
);
// Setup your Manager Registry or whatever...
$doctrineOrmExtension = new DoctrineOrmExtension($managerRegistry);
$builder->addExtension($doctrineOrmExtension);
When you use EntityType, myManagerRegistry#getService($name) will be called. $name is the name of the service it needs ('em' or 'connection') and it needs to return the Doctrine entity manager or the Doctrine database connection, respectively.
In your controller, try to call the service like that:
$em = $this->get('doctrine.orm.entity_manager');
Hope it will help you.
Edit:
Sorry, I thought you was on Symfony... I have too quickly read...

Symfony2 and FOSUserBundle: performing other operations when updating/creating a user

I'm using FOSUserBundle on Symfony2.
I extended the User class to have additional fields, therefore I also added the new fields in the twigs.
One of those fields is a licence code. When a user fills in that field I want to perform a connection to DB to look if that license is valid. If not returns an error, if yes creates an event in the "licenceEvents" table assigning the current user to that license.
[EDIT] As suggested I created a custom validator (which works like a charm), and I'm now struggling with the persisting something on DB once the user is created or updated.
I created an event listener as follows:
<?php
// src/AppBundle/EventListener/UpdateOrCreateProfileSuccessListener.php
namespace AppBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Doctrine\ORM\EntityManager; //added
class UpdateOrCreateProfileSuccessListener implements EventSubscriberInterface
{
private $router;
public function __construct(UrlGeneratorInterface $router, EntityManager $em)
{
$this->router = $router;
$this->em = $em; //added
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_COMPLETED => array('onUserCreatedorUpdated',-10),
FOSUserEvents::PROFILE_EDIT_COMPLETED => array('onUserCreatedorUpdated',-10),
);
}
public function onUserCreatedorUpdated(FilterUserResponseEvent $event)
{
$user = $event->getUser();
$code = $user->getLicense();
$em = $this->em;
$lastEvent = $em->getRepository('AppBundle:LicenseEvent')->getLastEvent($code);
$licenseEvent = new LicenseEvent();
// here I set all the fields accordingly, persist and flush
$url = $this->router->generate('fos_user_profile_show');
$event->setResponse(new RedirectResponse($url));
}
}
My service is like follows:
my_user.UpdateOrCreateProfileSuccess_Listener:
class: AppBundle\EventListener\UpdateOrCreateProfileSuccessListener
arguments: [#router, #doctrine.orm.entity_manager]
tags:
- { name: kernel.event_subscriber }
The listener is properly triggered, manages to create the connection to DB as expected, but gives me the following error
Catchable Fatal Error: Argument 1 passed to AppBundle\EventListener\UpdateOrCreateProfileSuccessListener::onUserCreatedorUpdated()
must be an instance of AppBundle\EventListener\FilterUserResponseEvent,
instance of FOS\UserBundle\Event\FilterUserResponseEvent given
I must be missing something very stupid...
Another question is: I don't want to change the redirect page, so that if the original page was the "email sent" (after a new user is created) let's go there, otherwise if it's a profile update show the profile page.

JMS Serializer: Serialize custom properties of entities

I want to add a custom property to the serialized entity's representation, which takes an existing entity property and formats it in a user friendly way by using an existing service.
I defined a subscriber class and injected the service used for formatting the existing entity property and subscribed to serializer.pre_serialize as follows:
class UserSerializationSubscriber implements EventSubscriberInterface
{
private $coreTwigExtension;
private $user;
public function setCoreTwigExtension(TwigExtension $coreTwigExtension)
{
$this->coreTwigExtension = $coreTwigExtension;
}
public function setUserService(UserService $user)
{
$this->user = $user;
}
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.pre_serialize',
'method' => 'onObjPreSerialize',
'class' => 'Some\Bundle\Entity\EntityClass',
'format' => 'json'
)
);
}
public function onObjPreSerialize(PreSerializeEvent $event)
{
$context = $event->getContext();
$context->attributes->get('groups')->map(
function(array $groups) use ($event) {
if (in_array('somegroup', $groups)) {
$obj= $event->getObject();
if ($obj->getConfirmedOn()) {
$contextualDate = $this->coreTwigExtension->getContextualDate($obj->getConfirmedOn());
$event->getVisitor()->addData('displayConfirmedOn', $contextualDate);
}
}
}
);
}
}
Subscriber registration:
some_bundle.handler.serialization:
class: Some\Bundle\Handler\ObjectSerializationSubscriber
calls:
- [setCoreTwigExtension, ['#bundle_core.twig.extension']]
- [setUserService, ['#some_bundle.service.user']]
tags:
- { name: jms_serializer.event_subscriber }
When I serialize an array/collection of entity Some\Bundle\Entity\EntityClass I get the following error:
There is already data for "displayConfirmedOn".
How do I resolve this? The only thing stopping me from using #VirtualProperty in the entity is that the virtual property output depends on a service, and no dependencies should be injected into an entity.
The error is due to the fact that the entity itself already exposes an attribute displayConfirmedOn for serialization. When your event listener runs it is not allowed to add an attribute with the same name to the output and you get this error.
Simply stop exposing the attribute in your entity and then the listener can add a property of the same name.

Best way to develeop plugin compatible application. Dependency injection?

I'm wondering the best way to create fully compatible application to plug-ins.
I'm used to Wordpress plug-ins concept that you can define actions and filters and then use in your plug-ins. So others can define methods on their plug-ins that are executed when the action is called (or the filter).
My idea is create my app with some actions and filters and then other developers can build a Bundle that interfere in the "normal" app flow...
I was reading about Symfony2 Dependency Injection, but I didn’t found some comprehensive example to do something similar that I want.
Someone has a real example of something similar that I'm looking for?
Is the Dependency Injection the best solution or should I build my own plugin handler?
EDIT:
What I did to allow other bundles to add items to my knp-menu menu.
In my base bundle:
Defining the filter that allow subscribber to get and set menu data:
# BaseBundle/Event/FilterMenuEvent.php
class FilterMenuEvent extends Event
{
protected $menu;
public function __construct($menu)
{
$this->menu = $menu;
}
public function getMenu()
{
return $this->menu;
}
}
Defining the events of the menu:
# Event/MenuEvents.php
final class MenuEvents
{
const BEFORE_ITEMS = 'menu.before.items';
const AFTER_ITEMS = 'menu.after.items';
}
Setting up the subscriber:
# Event/MenuSubscriber.php
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array(
array('homeItems', 9000),
array('quickactionsItems', 80),
array('adminItems', 70),
...
array('logoutItems', -9000),
)
);
}
public function homeItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Home', array('route' => 'zashost_zaspanel_homepage'));
}
public function quickactionsItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Quick actions', array( 'route' => null));
$menu['Quick actions']->addChild('Add hosting', array( 'route' => 'zashost_zaspanel_register_host'));
}
}
Dispatching events in the generation of menu:
# Menu\Builder.php
class Builder extends ContainerAware
{
public function userMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$this->container->get('event_dispatcher')->dispatch(MenuEvents::AFTER_ITEMS , new FilterMenuEvent($menu));
return $menu;
}
}
Attach subscriber to kernel event subscriber:
# services.yml
services:
# Menu items added with event listener
base_menu_subscriber:
class: Acme\BaseBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
Then in third party bundle:
Setting up my third party event subscriber:
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array('afterItems', 55)
);
}
public function afterItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Backups', array( 'route' => null));
$menu['Backups']->addChild('Create new backup', array( 'route' => null));
return $menu;
}
}
And attaching to kernel event subscriber:
# srevices.yml
services:
menu_subscriber:
class: Acme\ThirdPartyBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
In that way I can use the priority of Event Dispatcher to set the position of each group of items of the menu.
A good starting point in providing extension points for your application, in which other developers can hook their custom behaviour, is to use the EventDispatcher component from Symfony - a implementation of the Observer Pattern.
Symfony already uses the component extensively in it's own core ( HttpKernel ) to allow other components (or plugins, if you will) to hook in various points in the http request -> response flow and handle everything from Request matching to Response generation.
For example you can hook to the kernel.request event and return a Response immediately if the Request is not valid or to the kernel.response event and change the response content.
See the full list of default KernelEvents.
By only using these (there are many others related to other components), you can create a plugin sytem that is more capable, more testable and more robust than that of the Wordpress "platform".
Of course, you can easily create and dispatch your own events that will suit your business logic (for example create events like post.created or comment.created) for a blog application.
Now, for the sake of an example, here is how you will configure a "plugin" that will do something with the generated Response and then will fire another event (that can be used by another plugin)
namespace Vendor;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class ResponseAlter implements EventSubscriberInterface
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function doSomethingWithResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
/**
* let other plugins hook to the provide.footer event and
* add the result to the response
*/
$footer = new ProvideFooterEvent();
$this->dispatcher->dispatch('provide.footer', $footer);
$this->addFooterProvidedByPluginToResponse($response, $footer->getProvidedFooter());
$event->setResponse($response);
}
static function getSubscribedEvents()
{
return array(
'kernel.response' => 'doSomethingWithResponse'
);
}
}
Now you will simply have to tag your service as a service subscriber and you're done. You've just plugged in the HttpKernel component:
services:
my_subscriber:
class: Vendor\ResponseAlter
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}

Resources