Symfony2 : Automatically map query string in Controller parameter - symfony

In Symfony2, the route parameters can be automatically map to the controller arguments, eg: http://a.com/test/foo will return "foo"
/**
* #Route("/test/{name}")
*/
public function action(Request $request, $name) {
return new Response(print_r($name, true));
}
see http://symfony.com/doc/current/book/routing.html#route-parameters-and-controller-arguments
But I want to use query string instead eg: http://a.com/test?name=foo
How to do that ?
For me there are only 3 solutions:
re-implement ControllerResolverInterface
use a custom ParamConverter
$name = $request->query->get('name');
Is there another solution ?

I provide you the code for those which want to use a converter :
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Put specific attribute parameter to query parameters
*/
class QueryStringConverter implements ParamConverterInterface{
public function supports(ParamConverter $configuration) {
return 'querystring' == $configuration->getConverter();
}
public function apply(Request $request, ParamConverter $configuration) {
$param = $configuration->getName();
if (!$request->query->has($param)) {
return false;
}
$value = $request->query->get($param);
$request->attributes->set($param, $value);
}
}
services.yml :
services:
querystring_paramconverter:
class: AppBundle\Extension\QueryStringConverter
tags:
- { name: request.param_converter, converter: querystring }
In your controller:
/**
* #Route("/test")
* #ParamConverter("name", converter="querystring")
*/
public function action(Request $request, $name) {
return new Response(print_r($name, true));
}

An improved solution based on Remy's answer which will map the parameter to an entity :
<?php
namespace AppBundle\Extension;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter;
/**
* Put specific attribute parameter to query parameters
*/
class QueryStringConverter extends DoctrineParamConverter {
protected function getIdentifier(Request $request, $options, $name)
{
if ($request->query->has($name)) {
return $request->query->get($name);
}
return false;
}
}
services.yml:
services:
querystring_paramconverter:
class: MBS\AppBundle\Extension\QueryStringConverter
arguments: ['#doctrine']
tags:
- { name: request.param_converter, converter: querystring }
in your controller:
/**
* #Route("/test")
* #ParamConverter("myobject")
*/
public function action(Request $request, AnyEntity $myobject) {
return new Response(print_r($myobject->getName(), true));
}

like #2, To solve private method (getIdentifier) first set attributes and execute normally (parent::apply). Tested on Symfony 4.4
<?php
namespace App\FrameworkExtra\Converters;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter;
use Symfony\Component\HttpFoundation\Request;
class QueryStringEntityConverter extends DoctrineParamConverter
{
public function supports(ParamConverter $configuration)
{
return 'querystringentity' == $configuration->getConverter();
}
public function apply(Request $request, ParamConverter $configuration)
{
$param = $configuration->getName();
if (!$request->query->has($param)) {
return false;
}
$value = $request->query->get($param);
$request->attributes->set($param, $value);
return parent::apply($request, $configuration);
}
}

I havn't checked, but it seems that the FOSRestBundle provides the #QueryParam annotation which does that :
http://symfony.com/doc/current/bundles/FOSRestBundle/param_fetcher_listener.html

Related

Do Subscribers work while loading Fixtures in Symfony?

I tried to run the fixture below on Symfony 5 using the command php bin/console d:f:l.
I get this error: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'contact_email' cannot be null
The same code logic is working fine for Post entities when creating them manually through the CRUD. Are fixtures not compatible with subscribers (events) or did i make a mistake?
Thank you.
Edit: I'm also using EasyAdmin Bundle 3.
App\DataFixtures.php\AppFixtures.php
<?php
namespace App\DataFixtures;
use App\Entity\User;
use App\Entity\Author;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AppFixtures extends Fixture
{
/** #var User[] */
private $users = [];
/** #var Author[] */
private $authors = [];
/** #var UserPasswordHasherInterface */
private $hasher;
public function __construct(UserPasswordHasherInterface $hasher)
{
$this->hasher = $hasher;
}
public function load(ObjectManager $manager): void
{
$this->createUsers();
foreach($this->users as $user) $manager->persist($user);
$this->createAuthors();
foreach($this->authors as $author) $manager->persist($author);
$manager->flush();
}
public function createUsers(): void
{
$admin = (new User)
->setUsername('admin')
->setEmail('admin#admin.com')
->setRoles(['ROLE_ADMIN'])
->setFirstname('Edouard')
->setLastname('Proust');
$admin->setPassword($this->hasher->hashPassword($admin, 'admin'));
$this->users[] = $admin;
}
public function createAuthors(): void
{
foreach($this->users as $user) {
if(in_array('ROLE_ADMIN', $user->getRoles())) {
$author = (new Author)
->setUser($user)
->setAvatar('#')
->setBio('Bio')
// The line i want to get rid of:
// ->setContactEmail($user->getEmail())
;
$this->authors[] = $author;
}
}
}
}
App\EventListener\AuthorSubscriber.php
<?php
namespace App\EventListener;
use App\Entity\Author;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
class AuthorSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
BeforeEntityPersistedEvent::class => 'setContactEmail',
];
}
public function setContactEmail(BeforeEntityPersistedEvent $event)
{
/** #var Author */
$entity = $event->getEntityInstance();
if($entity instanceof Author) {
if(!$entity->getContactEmail()) {
$user = $entity->getUser();
$contactEmail = $user ? $user->getEmail() : '#';
$entity->setContactEmail($contactEmail);
}
}
}
}
EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent:class is not proper Symfony event name. You probably should use Doctrine\ORM\Events::prePersist.
Also please check your DoctrineBundle version. If you're using the default services.yaml configuration and DoctrineBundle lower than 2.1, you have to configure services.yaml with:
App\EventListener\AuthorSubscriber:
tags:
- name: 'doctrine.event_subscriber'
You can read something more here: https://symfony.com/doc/current/doctrine/events.html#doctrine-lifecycle-subscribers

Extend Doctrine EntityRepository

I have written a class BasicRepository in order to use it instead of the EntityRepository to add some basic modification like remove all deleted-flaged items.
<?php
namespace AppBundle\Repository;
use AppBundle\DataFixtures\ORM\LoadEventPrioData;
use AppBundle\Entity\Location;
use Doctrine\ORM\EntityRepository;
class BasicRepository extends EntityRepository
{
public function createQueryBuilder($alias, $indexBy = null)
{
$query = parent::createQueryBuilder($alias);
dump(parent::getClassName());
dump($this->getClassName());
if (property_exists($this->getClassName(), 'isDeleted')) {
dump("Ping");
$query->andWhere($alias.'.isDeleted = :false')->setParameter('false', false);
}
else {
dump("Pong");
}
return $query;
}
}
Controller:
...
public function searchAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$meta = new ClassMetadata('AppBundle:Location');
$er = new BasicRepository($em, $meta);
$query = $er->createQueryBuilder('u');
...
My aim is that - if the property "isDeleted" (boolean) exists in the Entity - the Query should contain an additional Where-Statement.
For some strange reason property_exists always return false - even when the property exits in the class.
I get your idea. The correct place you're looking for is Doctrine Filters. Check this package: https://github.com/DeprecatedPackages/DoctrineFilters#usage
There you can find example exactly with your use case:
<?php
use Doctrine\ORM\Mapping\ClassMetadata;
use Symplify\DoctrineFilters\Contract\Filter\FilterInterface;
final class SoftdeletableFilter implements FilterInterface
{
/**
* {#inheritdoc}
*/
public function addFilterConstraint(ClassMetadata $entity, $alias)
{
if ($entity->getReflectionClass()->hasProperty('isDeleted')) {
return "$alias.isDeleted = 0";
}
return '';
}
}

How to disable a doctrine filter in a param converter

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);
}

How to get Request object inside a Twig Extension in Symfony?

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,
]),
];
}
...

How can I create a symfony twig filter?

For instance my bundle namespace is Facebook\Bundle\FacebookBundle\Extension.
Using this how can I create a twig extension ?
It's all here: How to write a custom Twig Extension.
1. Create the Extension:
// src/Facebook/Bundle/Twig/FacebookExtension.php
namespace Facebook\Bundle\Twig;
use Twig_Extension;
use Twig_Filter_Method;
class FacebookExtension extends Twig_Extension
{
public function getFilters()
{
return array(
'myfilter' => new Twig_Filter_Method($this, 'myFilter'),
);
}
public function myFilter($arg1, $arg2='')
{
return sprintf('something %s %s', $arg1, $arg2);
}
public function getName()
{
return 'facebook_extension';
}
}
2. Register an Extension as a Service
# src/Facebook/Bundle/Resources/config/services.yml
services:
facebook.twig.facebook_extension:
class: Facebook\Bundle\Twig\AcmeExtension
tags:
- { name: twig.extension }
3. Use it
{{ 'blah'|myfilter('somearg') }}
You can also create twig functions by using the getFunctions()
class FacebookExtension extends Twig_Extension
{
public function getFunctions()
{
return array(
'myFunction' => new Twig_Filter_Method($this, 'myFunction'),
);
}
public function myFunction($arg1)
{
return $arg1;
}
Use your function like this:
{{ myFunction('my_param') }}
The Twig_Filter_Method class is DEPRECATED since Symfony 2.1
Please use the Twig_SimpleFilter class instead as showed in the following example:
\src\Acme\Bundle\CoreBundle\Twig\DatetimeExtension.php
<?php
namespace Acme\Bundle\CoreBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DatetimeExtension extends \Twig_Extension
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFilters()
{
return array(
'dateFormat' => new \Twig_SimpleFilter('dateFormat', array($this, 'dateFormat')),
'datetimeFormat' => new \Twig_SimpleFilter('datetimeFormat', array($this, 'datetimeFormat'))
);
}
/**
* #param mixed $date
* #return string
*/
public function dateFormat($date)
{
$format = $this->container->getParameter('acme_core.date_format');
return $this->format($date, $format);
}
/**
* #param mixed $date
* #return string
*/
public function datetimeFormat($date)
{
$format = $this->container->getParameter('acme_core.datetime_format');
return $this->format($date, $format);
}
/**
* #param mixed $date
* #param string $format
* #throws \Twig_Error
* #return string
*/
private function format($date, $format)
{
if (is_int($date) || (is_string($date) && preg_match('/^[0-9]+$/iu', $date))) {
return date($format, intval($date, 10));
} else if (is_string($date) && !preg_match('/^[0-9]+$/', $date)) {
return date($format, strtotime($date));
} else if ($date instanceof \DateTime) {
return $date->format($format);
} else {
throw new \Twig_Error('Date or datetime parameter not valid');
}
}
public function getName()
{
return 'datetime_extension';
}
}
\src\Acme\Bundle\CoreBundle\Resources\config\services.yml
services:
acme_core.twig.datetime_extension:
class: Acme\Bundle\CoreBundle\Twig\DatetimeExtension
arguments: [#service_container]
tags:
- { name: twig.extension }
Usage example:
{{ value|datetimeFormat }}
Symfony documentation: http://symfony.com/doc/master/cookbook/templating/twig_extension.html
Twig documentation: http://twig.sensiolabs.org/doc/advanced.html#id3
None of the given answers worked for Symfony 3.4 and above.
For Symfony 3.4, 4.x
// src/TwigExtension/customFilters.php
namespace App\TwigExtension;
use Twig\TwigFilter;
class customFilters extends \Twig_Extension {
public function getFilters() {
return array(
new TwigFilter('base64_encode', array($this, 'base64_en'))
);
}
public function base64_en($input) {
return base64_encode($input);
}
}
And then in your twig template you can do
{{ 'hello world' | base64_encode }}
Thats it. For detailed explanation, you could check out the reference.
Reference: DigitalFortress

Resources