I would like to add one action to a page (let's say the route entity.node.canonical) but this action would appear and disappear from the page time to time.
So what I'm trying to do is to create the action using a deriver (I put my condition to show the action in the method getDerivativeDefinitions) and then, on a kernel event, I refresh the local actions using \Drupal::service('plugin.manager.menu.local_action')->clearCachedDefinitions();
But it still doesn't work !
So is anybody capable of showing me a method to show and hide an action ?
Here is my derivative:
class ConditionalAction extends DeriverBase implements ContainerDeriverInterface {
/**
* {#inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition)
{
// If the seconds is more than 30, hide the action.
if (date('s') > 30) {
return $this->derivatives;
}
// Else, show the action.
$menu_entry = $base_plugin_definition;
$menu_entry['route_name'] = 'my.route.name';
$menu_entry['title'] = 'Test action';
$menu_entry['appears_on'][] = \Drupal::routeMatch()->getRouteName();
$menu_entry['route_parameters'] = ['time' => date('s')];
$this->derivatives['my.action.name'] = $menu_entry;
return $this->derivatives;
}
}
And here is the event subscriber:
class MyModuleKernelSubscriber implements EventSubscriberInterface {
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => 'onKernelRequest'
];
}
/**
* Event called when a request is sent.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event) {
// Do not consider the ajax requests.
$request = $event->getRequest();
if ($request->isXmlHttpRequest() === TRUE) {
return;
}
// Flush local action cache.
\Drupal::service('plugin.manager.menu.local_action')->clearCachedDefinitions();
}
}
So, here, on each request, the local action should be cleared and the code should go through my derivative. But it's not working... Any thought ?
I really need it to be an action ! And I really need the URL argument to be dynamic as well (here, $menu_entry['route_parameters'] = ['time' => date('s')];).
Thank you
By looking inside the console command drupal cache:reset, I found that the lines
$bins = Cache::getBins();
$bins['render']->deleteAll();
$bins['discovery']->deleteAll();
reset the local actions on each requests.
So I have my answer now.
Hope this will help !
Related
I'm learnig the event Subscriber on Symfony, my idea is when I create a new blog post, I want to send a notification. I'm already able to get the last entity created.
I've added to code to get the Symfony notifier and be able to send a notification on Disocrd. I currently have the following code :
class EasyAdminSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
AfterEntityPersistedEvent::class => ['setBlogPostSlug'],
];
}
/**
* #param AfterEntityPersistedEvent $event
* #param ChatterInterface $chatter
* #return void
* #throws \Symfony\Component\Notifier\Exception\TransportExceptionInterface
*/
public function setBlogPostSlug(AfterEntityPersistedEvent $event)
{
$chatter = new Chatter();
$entity = $event->getEntityInstance();
if (!($entity instanceof Blog)) {
return;
}
$message = (new ChatMessage('You got a new invoice for 15 EUR.'));
// if not set explicitly, the message is send to the
// default transport (the first one configured)
$chatter->send($message);
}
}
But when I run the code I have the followinf error :
App\EventSubscriber\EasyAdminSubscriber::setBlogPostSlug(): Argument #2 ($chatter) must be of type Symfony\Component\Notifier\ChatterInterface, string given
I don't understand what's wrong. Do you any idea why ? And how to make things work ?
I'm trying to get all the errors from all the queries I do in my project and redirect this errors to a controller called "error" that will treat theses errors as I want. The problem looks like when I redirect all the information goes in the url generated by the function via GET.
I suppose that if this information is sent via POST will disappear this problem but I'm not using obviously any form inside the controller. So, how can I say to the redirect function that these information shouldn't go with the url and instead should go via POST?
Is possible what I'm trying to do?
Inside Controllers:
try {
$results = $queries->aQuery();
} catch (ErrorException $errorException) {
return $this->redirect($errorException->redirectResponse);
}
Inside the service query:
public function aQuery(){
$query="SELECT * FROM blabla ...";
try {
$stmt = $this->DB->->prepararQuery($query);
$stmt->execute();
$results = $stmt->fetchAll();
} catch (DBALException $DBALException) {
$errorException = new ErrorException($this->router->generate('error',
[
'errorQuery' => $query,
'errorData' => "0 => '".$data1."', 1 ....",
'errorOrigin' => 'a place',
'errorResponseText' => $DBALException->getMessage()
]
));
throw $errorException;
}
}
The ErrorException:
class ErrorException extends \Exception
{
/**
* #var \Symfony\Component\HttpFoundation\RedirectResponse
*/
public $redirectResponse;
/**
* ErrorException constructor.
* #param \Symfony\Component\HttpFoundation\RedirectResponse $redirectResponse
*/
public function __construct(string $redirectResponse)
{
$this->redirectResponse = $redirectResponse;
}
}
If what you are trying to achieve is a centralized way to handle exceptions have a look at https://symfony.com/doc/4.0/event_dispatcher.html#creating-an-event-listener and use kernel.exception event
public function onKernelException(GetResponseForExceptionEvent $event)
{
if (! $event->getException() instanceof ErrorException) {
return;
}
// handle your custom ErrorException
$response = new Response();
$response->setContent($event->getException()->getMessage());
// sends the modified response object to the event
$event->setResponse($response);
}
I have an Event Subscriber with function setAcademicCalendar. I want to catch exceptions, display an error message in the flash bag and terminate form submit. Basically, I want to stay in the form (no redirects), give the user an error message and don't save the form.
I have two problems. 1. I don't know to terminate the process 2. The flash message is only displayed after a page refresh.
private function setAcademicCalendar(FormEvent $event) {
/** #var CalendarEvent $calendar_event */
$calendar_event = $event->getData();
if ($calendar_event->getCalendar() instanceof Calendar) {
try {
$sem = $this
->container
->get('academic_calendar')
->getSemester($calendar_event->getStart());
$calendar_event->setSemester($sem);
} catch (\Exception $e) {
$this->container->get('session')->getFlashBag()->add('error', $e->getMessage());
}
}
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
FormEvents::SUBMIT => 'submitData',
FormEvents::PRE_SET_DATA => 'preSetData',
];
}
/**
* #param FormEvent $event
*/
public function submitData(FormEvent $event)
{
$this->setAcademicCalendar($event);
}
If you want to do this without redirection / refresh forget about validation in strict php. Only some strict frontend techs - like angular js , vue.js with some api calls
I added a VichImageType field on edit profile twig template...so im trying to check image dimensions using vich_uploader.pre_upload as Event.
In my class i got the image properties and if their dimensions arent bigger enough i tried to stop propagation and flashed a message to the twig template but, i dont know why, the event keeps propagating and redirects to fos_user_profile_show showing the image setup. Also, i tried to redirect again to fos_user_profile_edit but i cant use $event because "Vich\UploaderBundle\Event\Event" doesnt implement setController(). How can achieve this?
This is the method of the Listener class:
namespace BackendBundle\EventListener;
use Vich\UploaderBundle\Event\Event;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class ComprobarDimensionesDeImagen
{
private $requestStack;
private $router;
public function __construct(RequestStack $requestStack, RouterInterface $router)
{
$this->requestStack = $requestStack;
$this->router = $router;
}
public function onVichUploaderPreUpload(Event $event)
{
$object = $event->getObject();
$mapping = $event->getMapping();
$imagen = getimagesize($object->getImageFile());
if (250 > $imagen[0] || 250 > imagen[1]) {
$request = $this->requestStack->getCurrentRequest();
$session = $request->getSession();
$event->stopPropagation();
$session->getFlashBag()->add('error', "Minimum dimensions: 250x250 \n");
$url = $this->router->generate('fos_user_profile_edit');
/*
* Testing different methods of redirect
*
* $response = new RedirectResponse($url);
* $event->setResponse($response);
*/
$event->setController(function() use ($request) {
return new RedirectResponse($url);
});
}
}
}
When i edit the profile again, I can see the flash message and the image setup in VichImageType field (i didnt expect that stopping propagation). Any help will be very welcome.
SOLVED: Just using #Assert\Image in my Entity class did the validation. No service neither listener needed
The argument for ->setController must be a callable . In your case, the function you pass as an argument returns an object of type Response. In order to be callable, the method should have the suffix Action. See also this post.
I separated mobile and web requests with the help of kernel.view Event Listener.
The logic works like this:
if request is coming from mobile, then load xxx.mobile.twig
if request is coming from web, then load xxx.html.twig
This is working with my CustomBundle without any problem. In addition to it I'm using FOSUserBundle and HWIOAuthBundle with some of their routes. I checked var/logs/dev.log and I can't see kernel.view events regarding these bundles routes and eventually my listener cannot work with these bundles.
Could you give me an idea how could I bind to the kernel.view event for those bundles?
/**
* #param GetResponseForControllerResultEvent $event
* #return bool
*/
public function onKernelView(GetResponseForControllerResultEvent $event)
{
if (!$this->isMobileRequest($event->getRequest()->headers->get('user-agent'))) {
return false;
}
$template = $event->getRequest()->attributes->get('_template');
if (!$template) {
return false;
}
$templateReference = $this->templateNameParser->parse($template);
if ($templateReference->get('format') == 'html' && $templateReference->get('bundle') == 'CustomBundle') {
$mobileTemplate = sprintf(
'%s:%s:%s.mobile.twig',
$templateReference->get('bundle'),
$templateReference->get('controller'),
$templateReference->get('name')
);
if ($this->templating->exists($mobileTemplate)) {
$templateReference->set('format', 'mobile');
$event->getRequest()->attributes->set('_template', $templateReference);
}
}
}
There are a few things you should consider when debugging event related issues on Symfony2.
Events propagation can be stopped
it could be that another listener is listening for the very same event and is stopping the event propagation by calling $event->stopPropagation(). In that case make sure your listener is executed first (see point 2 below).
Event listeners have priorities
When defining a listener you can set its priority like shown below:
view_response_listener:
class: AppBundle\EventListener\ViewResponseListener
tags:
# The highest the priority, the earlier a listener is executed
# #see http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
- { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 101 }
The other optional tag attribute is called priority, which defaults to 0 and it controls the order in which listeners are executed (the highest the priority, the earlier a listener is executed). This is useful when you need to guarantee that one listener is executed before another. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer.
Source: http://symfony.com/doc/2.7/cookbook/event_dispatcher/event_listener.html#creating-an-event-listener
Usually the dispatch of those events is done in the bootstrap file
Make sure regenerate your bootstrap file (and clear the cache now that you're at it!), especially if you're playing with priorities and/or your configuration.
composer run-script post-update-cmd
php app/console cache:clear --env=dev
The composer post-update-cmd will regenerate your bootstrap file but it will also do other things like reinstalling your assets which is probably something that you don't need. To just regenerate the bootstrap file check my answer here.
I find the solution, it is however a bit workaround, working properly now.
I put following function to my MobileTemplateListener.php file.
More details are here -> http://www.99bugs.com/handling-mobile-template-switching-in-symfony2/
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($this->isMobileRequest($request->headers->get('user-agent')))
{
//ONLY AFFECT HTML REQUESTS
//THIS ENSURES THAT YOUR JSON REQUESTS TO E.G. REST API, DO NOT GET SERVED TEXT/HTML CONTENT-TYPE
if ($request->getRequestFormat() == "html")
{
$request->setRequestFormat('mobile');
}
}
}
/**
* Returns true if request is from mobile device, otherwise false
* #return boolean mobileUA
*/
private function isMobileRequest($userAgent)
{
if (preg_match('/(android|blackberry|iphone|ipad|phone|playbook|mobile)/i', $userAgent)) {
return true;
}
return false;
}
as a result when kernel.request event listener starts to handling, it is setting the format with value mobile
I was using FOSUserBundle through a child bundle to manipulate for my needs. You may find more details here -> https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/overriding_controllers.md
for instance : we assume SecurityController.php
I created a file named SecurityController.php under my UserBundle It looks like following.
<?php
namespace Acme\UserBundle\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Acme\UserBundle\Overrides\ControllerOverrideRenderTrait;
class SecurityController extends BaseController
{
use ControllerOverrideRenderTrait;
public function loginAction(Request $request)
{
$securityContext = $this->container->get('security.context');
$router = $this->container->get('router');
if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
return new RedirectResponse($router->generate('my_profile_dashboard'), 307);
}
return parent::loginAction($request);
}
}
for all other FOS controllers I have to override render function. Which is provided by smyfony Symfony\Bundle\FrameworkBundle\Controller
But already my child bundle extends the FOSUserBundle's controllers, the only way to override this without duplicates of code is to use traits.
and I created one trait as following.
<?php
namespace Acme\UserBundle\Overrides;
use Symfony\Component\HttpFoundation\Response;
trait ControllerOverrideRenderTrait {
/**
* This overrides vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
$format = $this->getRequest()->getRequestFormat();
$view = str_replace('.html.', '.' . $format . '.', $view);
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
}
The Original function provided by symfony is the following.
/**
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*/
public function render($view, array $parameters = array(), Response $response = null)
{
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}
Basically my change replaces '.html.' part in template name by providing $format which setted via onKernelRequest EventListener.
don't forget to add your service definition in services.yml
services:
acme.frontend.listener.mobile_template_listener:
class: Acme\FrontendBundle\EventListener\MobileTemplateListener
arguments: ['#templating', '#templating.name_parser']
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }