How can one access the Request object inside Twig Extension?
namespace Acme\Bundle\Twig;
use Twig_SimpleFunction;
class MyClass extends \Twig_Extension
{
public function getFunctions()
{
return array(
new Twig_SimpleFunction('xyz', function($param) {
/// here
$request = $this->getRequestObject();
})
);
}
public function getName() {
return "xyz";
}
}
As requested in the comments, here's the prefered way of injecting a request into any service. It works with Symfony >= 2.4.
Injecting the request and putting our service in the request scope is no longer recommended. We should use the request stack instead.
namespace AppBundle\Twig;
use Symfony\Component\HttpFoundation\RequestStack;
class MyClass extends \Twig_Extension
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getFunctions()
{
$requestStack = $this->requestStack;
return array(
new \Twig_SimpleFunction('xyz', function($param) use ($requestStack) {
$request = $requestStack->getCurrentRequest();
})
);
}
public function getName()
{
return "xyz";
}
}
app/config/services.yml
app.twig_extension:
class: AppBundle\Twig\MyExtension
arguments:
- '#request_stack'
tags:
- { name: twig.extension }
Docs:
the request stack API
the request stack announcement
Register your extension as a service and give it the container service:
# services.yml
services:
sybio.twig_extension:
class: %sybio.twig_extension.class%
arguments:
- #service_container
tags:
- { name: twig.extension, priority: 255 }
Then retrieve the container by your (twig extension) class constructor and then the request:
<?php
// Your class file:
// ...
class MyClass extends \Twig_Extension
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var Request
*/
protected $request;
/**
* Constructor
*
* #param ContainerInterface $container
*/
public function __construct($container)
{
$this->container = $container;
if ($this->container->isScopeActive('request')) {
$this->request = $this->container->get('request');
}
}
// ...
Note that testing the scope is usefull because there is no request when running console command, it avoids warnings.
That's it, you are able to use the request !
I would suggest setting 'needs_environment' => true for your Twig_SimpleFunction, which then will add \Twig_Environment as first argument of your function. Then in your function you can find the request like this:
$request = $twig->getGlobals()['app']->getRequest();
So the whole function will look like this:
...
public function getFunctions() {
return [
new \Twig_SimpleFunction('xyz', function(\Twig_Environment $env) {
$request = $twig->getGlobals()['app']->getRequest();
}, [
'needs_environment' => true,
]),
];
}
...
Related
i'm working on Symfony 4.
I have a service:
App\EventListener\EbayExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.controller }
arguments: [ "#doctrine.orm.entity_manager" , "#router" , "#session" ]
And here is my listener :
<?php
namespace App\EventListener;
use App\Entity\Ebay;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Routing\Router;
class EbayExceptionListener
{
private $repository;
private $router;
private $session;
private $redirectName = 'home';
public function __construct(EntityManager $entityManager, Router $router, Session $session)
{
$this->repository = $entityManager->getRepository(Ebay::class);
$this->router = $router;
$this->session = $session;
}
//Check if an eBay entity exist in the database, if not redirect to the form to create the eBay entity
public function onKernelController(FilterControllerEvent $event)
{
$ebay = $this->repository->findOneBy(['name' => 'Ebay']);
$request = $event->getRequest();
$routeName = $request->get('_route');
dump($routeName);
if ($routeName != "home" AND $ebay == null) {
$this->session->getFlashBag()->add('error', 'Please provide information for Ebay form');
return new RedirectResponse($this->router->generate($this->redirectName));
}
}
}
My Controller:
<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HomeController extends Controller
{
/**
* #Route("/", name="home")
*/
public function index()
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
]);
}
}
I have a problem with the var $routeName inside my Listener, in fact the value of this variable should be the name of the current route. But when I use the dump($routeName)
I have 2 results : "_wdt" and "home"
I don't know why the $request parameters are not the same when my listener is executed and after the page has been generated.
Because I have no page call "_wdt" it supposed to be "home".
Did I do something wrong ?
Thank you for your help.
I need to redirect from the controller rendered in twig, which is available in the example along the route "/login_form"
On production don't work, on dev work, i'm confused as to how to get this code to work in the producion?
Symfony 3.4
The code is made only as an example for the test
services.yml:
services:
kernel.listener.redirectexception:
class: AppBundle\EventListener\RedirectExceptionListener
arguments:
- "#router"
tags:
- { name: kernel.event_listener, event: kernel.exception }
DefaultController.php:
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Exception\RedirectException;
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.project_dir')).DIRECTORY_SEPARATOR,
]);
}
/**
* #Route("/login", name="login")
*/
public function loginAction(Request $request)
{
if ($this->getUser()) {
throw new RedirectException('This user does not have access to this section.');
}
return $this->render('default/login.html.twig', []);
}
/**
* #Route("/login_form", name="login_form")
*/
public function loginFormAction(Request $request)
{
return $this->render('default/login_form.html.twig', []);
}
}
RedirectExceptionListener.php:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use AppBundle\Exception\RedirectException;
use Symfony\Component\Routing\RouterInterface;
class RedirectExceptionListener
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
if ((!$event->getException()->getPrevious() instanceof RedirectException)
&& (!$event->getException() instanceof RedirectException)) {
return;
}
$response = new RedirectResponse($this->router->generate('homepage'));
$event->setResponse($response);
}
}
login_form.html.twig:
{{ render(controller('AppBundle:Default:login')) }}
I'm using the doctrine softdeleteable extension on a project and have my controller action set up as such.
/**
* #Route("address/{id}/")
* #Method("GET")
* #ParamConverter("address", class="MyBundle:Address")
* #Security("is_granted('view', address)")
*/
public function getAddressAction(Address $address)
{
This works great as it returns NotFound if the object is deleted, however I want to grant access to users with ROLE_ADMIN to be able to see soft deleted content.
Does there already exist a way to get the param converter to disable the filter or am I going to have to create my own custom param converter?
There are no existing ways to do it, but I've solved this problem by creating my own annotation, that disables softdeleteable filter before ParamConverter does its job.
AcmeBundle/Annotation/IgnoreSoftDelete.php:
namespace AcmeBundle\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* #Annotation
* #Target({"CLASS", "METHOD"})
*/
class IgnoreSoftDelete extends Annotation { }
AcmeBundle/EventListener/AnnotationListener.php:
namespace AcmeBundle\EventListener;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class AnnotationListener {
protected $reader;
public function __construct(Reader $reader) {
$this->reader = $reader;
}
public function onKernelController(FilterControllerEvent $event) {
if (!is_array($controller = $event->getController())) {
return;
}
list($controller, $method, ) = $controller;
$this->ignoreSoftDeleteAnnotation($controller, $method);
}
private function readAnnotation($controller, $method, $annotation) {
$classReflection = new \ReflectionClass(ClassUtils::getClass($controller));
$classAnnotation = $this->reader->getClassAnnotation($classReflection, $annotation);
$objectReflection = new \ReflectionObject($controller);
$methodReflection = $objectReflection->getMethod($method);
$methodAnnotation = $this->reader->getMethodAnnotation($methodReflection, $annotation);
if (!$classAnnotation && !$methodAnnotation) {
return false;
}
return [$classAnnotation, $classReflection, $methodAnnotation, $methodReflection];
}
private function ignoreSoftDeleteAnnotation($controller, $method) {
static $class = 'AcmeBundle\Annotation\IgnoreSoftDelete';
if ($this->readAnnotation($controller, $method, $class)) {
$em = $controller->get('doctrine.orm.entity_manager');
$em->getFilters()->disable('softdeleteable');
}
}
}
AcmeBundle/Resources/config/services.yml:
services:
acme.annotation_listener:
class: AcmeBundle\EventListener\AnnotationListener
arguments: [#annotation_reader]
tags:
- { name: kernel.event_listener, event: kernel.controller }
AcmeBundle/Controller/DefaultController.php:
namespace AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use AcmeBundle\Annotation\IgnoreSoftDelete;
use AcmeBundle\Entity\User;
class DefaultController extends Controller {
/**
* #Route("/{id}")
* #IgnoreSoftDelete
* #Template
*/
public function indexAction(User $user) {
return ['user' => $user];
}
}
Annotation can be applied to individual action methods and to entire controller classes.
You can use #Entity for this, customizing a repository method like this:
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
/**
* #Route("/{id}")
* #Entity("post", expr="repository.findDisableFilter(id)")
*/
public function disable(Post $post): JsonResponse
{
...
}
and then in your repository class:
public function findDisableFilter(mixed $id): mixed
{
$filterName = 'your-filter-name';
$filters = $this->getEntityManager()->getFilters();
if ($filters->has($filterName) && $filters->isEnabled($filterName)) {
$filters->disable($filterName);
}
return $this->find($id);
}
I have problems with adding Twig extensions.
I have Bundle controllers extending custom BaseController class:
class DefaultController extends BaseController
And there's my BaseController class (only part of it).
class BaseController extends Controller {
public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container = null)
{
parent::setContainer($container);
$this->onContainerSet();
}
public function onContainerSet()
{
// many other tasks
$this->get('twig')->addExtension(new \Twig_Extension_StringLoader());
$this->get('twig.loader')->addPath('../app');
$function = new \Twig_SimpleFunction('stars', function ($number, $maximum_stars = 5) {
$this->get('twig')->addGlobal('star_number',sprintf("%.1f",$number));
$this->get('twig')->addGlobal('star_max',$maximum_stars);
$full_stars = floor($number);
$half_stars = ($number - $full_stars) * 2;
$empty_stars = $maximum_stars - $full_stars - $half_stars;
$this->get('twig')->addGlobal('full_stars_number',$full_stars);
$this->get('twig')->addGlobal('half_stars_number',$half_stars);
$this->get('twig')->addGlobal('empty_stars_number',$empty_stars);
echo $this->renderView(
'views/stars.html.twig'
);;
});
$function2 = new \Twig_SimpleFunction('inurl', function ($anchor, $code) {
echo ''.$anchor."";
});
$this->get('twig')->addFunction($function);
$this->get('twig')->addFunction($function2);
}
}
The problem:
When I clear cache directory I have first message:
CRITICAL - Uncaught PHP Exception LogicException: "Unable to register
extension "string_loader" as extensions have already been
initialized." at ...\vendor\twig\twig\lib\Twig\Environment.php line
660 Context: {"exception":"Object(LogicException)"}
But when I reload page (cache folder is already created) it works fine (no exception).
However if I comment line:
// $this->get('twig')->addExtension(new \Twig_Extension_StringLoader());
and clear cache directory I have exception:
CRITICAL - Uncaught PHP Exception LogicException: "Unable to add
function "stars" as extensions have already been initialized." at
...\vendor\twig\twig\lib\Twig\Environment.php line 946 Context:
{"exception":"Object(LogicException)"}
So it seems that when cache directory doesn't exist from some reason adding any Twig extensions doesn't work (extensions have already been initialized) as I would like but when cache directory is already created everything works fine.
Question - how to solve it in the simplest way?
Create your class in YourBundle\Twig
class YourExtension extends \Twig_Extension
{
/**
* #var Router
*/
protected $router;
function __construct(Router $router)
{
$this->router = $router;
}
/**
* #return array
*/
public function getFilters()
{
return [
new \Twig_SimpleFilter('my_filter', [$this, 'myFilter'], ['is_safe' => ['html']]),
];
}
/**
* #return string
*/
public function myFilter(User $user)
{
return 'FILTERED: ' . $user->getName();
}
/**
* #return string
*/
public function getName()
{
return 'my_filter_extension';
}
}
Then, register your extension as a service: ( in this case I inject router as an argument )
yourbundle.twig.my_filter_extension:
class: Acme\YourBundle\Twig\YourExtension
arguments: [#router]
tags:
- { name: twig.extension }
If you want to enable Twig_Extension_StringLoader, add to your services:
yourbundle.twig.extension.loader:
class: Twig_Extension_StringLoader
tags:
- { name: 'twig.extension' }
Twig_Extension_StringLoader is not loaded by default.
What I finally did to achieve result (maybe someone will have similar problem in the future):
In config.yml I've added:
services:
yourbundle.twig.extension.loader:
class: Twig_Extension_StringLoader
tags:
- { name: 'twig.extension' }
yourbundle.twig.stars_extension:
class: Mnab\Twig\Stars
tags:
- { name: 'twig.extension' }
yourbundle.twig.inurl_extension:
class: Mnab\Twig\InternalUrl
tags:
- { name: 'twig.extension' }
in my BaseController I only left from question code:
$this->get('twig.loader')->addPath('../app');
but also added:
$this->get('twig')->addGlobal('internal_links',$this->internalLinks);
to use it in Twig extension
And I've create 2 classes:
<?php
//InternalUrl.php
namespace Mnab\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
class InternalUrl extends \Twig_Extension {
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('inurl', array($this, 'inUrlFunction'), array('needs_environment' => true, 'is_safe' => array('html'))),
);
}
public function inUrlFunction(\Twig_Environment $env, $anchor, $code)
{
return ''.$anchor."";
}
public function getName()
{
return 'inurl_extension';
}
}
and
<?php
// Stars.php
namespace Mnab\Twig;
class Stars extends \Twig_Extension
{
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('stars', array($this, 'starsFunction'), array('needs_environment' => true, 'is_safe' => array('html'))),
);
}
public function starsFunction(\Twig_Environment $env, $number, $maximum_stars = 5)
{
$env->addGlobal('star_number',sprintf("%.1f",$number));
$env->addGlobal('star_max',$maximum_stars);
$full_stars = floor($number);
$half_stars = ($number - $full_stars) * 2;
$empty_stars = $maximum_stars - $full_stars - $half_stars;
$env->addGlobal('full_stars_number',$full_stars);
$env->addGlobal('half_stars_number',$half_stars);
$env->addGlobal('empty_stars_number',$empty_stars);
return $env->render(
'views/stars.html.twig'
);
}
public function getName()
{
return 'stars_extension';
}
}
Now it seems to work regardless of cache is created or not. So it seems to better register services when you want to use Twig Extensions than registering Extensions in Controller.
First time I'm posting a message in this forum, which I use regularly. I use FOSUserbundle in my Symfony2 application to manage users. I activated the sending of the email confirmation when users create an account by the following thing:
fos_user:
registration:
confirmation:
enabled: true
It works very well: the email is sent successfully. I am redirected to the page /check-email that says that this email has been sent. However, I would like to change the redirection : I would like to be redirected to my index page and not to /check-email. So I did my research and I knew he had to go through the FOSUserBundle events (list here).
What I did :
class RegistrationListener implements EventSubscriberInterface {
private $router;
public function __construct(UrlGeneratorInterface $router) {
$this->router = $router;
}
public static function getSubscribedEvents() {
return array(
FOSUserEvents::REGISTRATION_CONFIRM => 'onRegistrationConfirm');
}
public function onRegistrationConfirm(FormEvent $event) {
$url = $this->router->generate('listeArticlesAccueil');
$event->setResponse(new RedirectResponse($url));
}
}
and services.yml
services:
listener_user.registration_listener:
class: My\Soft\UserBundle\EventListener\RegistrationListener
arguments: [#router]
tags:
- { name: kernel.event_subscriber }
The problem is that every time I am redirected to the page /check-email. I therefore told me that it was probably the wrong event. So I also tried REGISTRATION_SUCCESS. But nothing changes. So either I have not used the right event, or I'm doing something wrong.
In either case, I hope you can help me !
Thanks a lot and sorry for my bad english ;)
I know that this question has been posted a long time ago, but i found a solution and I would like to share it to others that have the same issue.
In this case, the event that you should use is REGISTRATION_SUCCESS.
You have to prioritize the listener REGISTRATION_SUCCESS to be called before FOS\UserBundle\EventListener\EmailConfirmationListener::onRegistrationSuccess like this:
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_SUCCESS => [
['onRegistrationSuccess', -10],
],
];
}
If you don't do it, EmailConfirmationListener will be called earlier and you will be redirected to fos_user_registration_check_email route.
Here is what I did:
class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onRegistrationSuccess(FormEvent $event)
{
$event->stopPropagation();
$url = $this->router->generate('homepage');
$response = new RedirectResponse($url);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess',-10],
];
}
}
And in app/services.yml:
app.redirect_after_registration_subscriber:
class: AppBundle\EventListener\RedirectAfterRegistrationSubscriber
arguments: ['#router']
tags:
- { name: kernel.event_subscriber }
I hope this helps.
I would bet on REGISTRATION_SUCCESS.
The doc (https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/FOSUserEvents.php) says
that this event allows to set the response.
REGISTRATION_CONFIRM only allows to access the user
Instead of that use REGISTRATION_CONFIRM.
Here is the complete code for the event listener:
/**
* Description of RegisterationConfirmListener
*
* #author imran
*/
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\GetResponseUserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Description of RegisterationConfirmListener
*
* #author imran
*/
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\GetResponseUserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class RegisterationConfirmListener implements EventSubscriberInterface {
private $router;
public function __construct(UrlGeneratorInterface $router) {
$this->router = $router;
}
public static function getSubscribedEvents() {
return [
FOSUserEvents::REGISTRATION_CONFIRM => 'onRegisterationConfirm',
];
}
public function onRegisterationConfirm(GetResponseUserEvent $event) {
$url = $this->router->generate('home');
$event->setResponse(new RedirectResponse($url));
}
}
For Services.yml:
app_user.registeration_confirmed:
class: Wishlocker\AppBundle\EventListener\RegisterationConfirmListener
arguments: [ #router ]
tags:
- { name: kernel.event_subscriber }