Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override router and add parameter to specific routes (before path/url used)

I would use a easy management routing system.

For example NOW i have this routes.

_welcome                    ANY      ANY    ANY  /
acmedemo_example_index      ANY      ANY    ANY  /acme/demos
acmedemo_example_edit       ANY      ANY    ANY  /acme/edit/{id}
acmedemo_example_delete     ANY      ANY    ANY  /acme/delete/{id}
acmeapi_backup_get          GET      ANY    ANY  /api/acme
acmeapi_backup_edit         POST     ANY    ANY  /api/acme

Now I would add the current user id to each route, because if a user send me or another supporter/administrator a link, we would see what the user see. You understand?

I would have this now.

_welcome                    ANY      ANY    ANY  /
acmedemo_example_index      ANY      ANY    ANY  /{user}/acme/demos
acmedemo_example_edit       ANY      ANY    ANY  /{user}/acme/edit/{id}
acmedemo_example_delete     ANY      ANY    ANY  /{user}/acme/delete/{id}
acmeapi_backup_get          GET      ANY    ANY  /api/acme
acmeapi_backup_edit         POST     ANY    ANY  /api/acme

And now the "problem"... I want to add the "user" parameter to each route automatically if the route name matches preg_match('/^acmedemo_/i').

For example (index.html.twig):

<a href="{{ path('acmedemo_example_index') }}">Show demos</a>

Or

<a href="{{ path('acmedemo_example_edit', {id: entity.id}) }}">Edit demo</a>

I NOT want to use {{ path('acmedemo_example_edit', {id: entity.id, user: app.user.id}) }}!

And the "user" parameter requires "\d+".

I would like to override the "generate" function on the router, for example. Then I could check if $router->getUrl() matches the ^acmedemo_ and then I could add the user parameter :)

Thanks!

like image 788
PatrickB Avatar asked Mar 19 '14 15:03

PatrickB


3 Answers

Actually there is a much simpler way to do that using RequestContext::setParameter() method. This context is available through the router via Router::getContext() method.

Use a kernel listener on incoming requests to initialize this context or, when outside of a request scope (command for example), directly by calling the method on the router service.

$router->getContext()->setParameter('user', $user->getId());

// where $router is the @router service.

Example of request listener:

namespace AppBundle\Listener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Add user param to router context on new request.
 */
class UserAwareRouterContextSubscriber implements EventSubscriberInterface
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var RequestContextAwareInterface
     */
    private $router;

    /**
     * @param TokenStorageInterface        $tokenStorage
     * @param RequestContextAwareInterface $router
     */
    public function __construct(TokenStorageInterface $tokenStorage, RequestContextAwareInterface $router)
    {
        $this->tokenStorage = $tokenStorage;
        $this->router = $router;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [KernelEvents::REQUEST => 'onRequest'];
    }

    /**
     * @param GetResponseEvent $event
     */
    public function onRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        if ($token = $this->tokenStorage->getToken()) {
            $user = $token->getUser();
            if ($user instanceof MyUserClass) { // use your own class here :)
                $this->router->getContext()->setParameter('user', $user->getId());
            }
        }
    }
}

Service configuration:

services:
    app.listener.user_aware_router_context:
        class: AppBundle\Listener\UserAwareRouterContextSubscriber
        arguments:
            - '@security.token_storage'
            - '@router'
        tags:
            - {name: kernel.event_subscriber}
like image 59
rolebi Avatar answered Oct 19 '22 22:10

rolebi


Soooo new day for me :D

I overrided the router and the UrlGenerator.

@Chausser: I fixed your problem 1 with an easy:

acme_demo_example:
    resource: "@AcmeDemoBundle/Controller/"
    type:     annotation
    prefix:   /{user}

Now I have routes like this.

_welcome                    ANY      ANY    ANY  /
acmedemo_example_index      ANY      ANY    ANY  /{user}/acme/demos
acmedemo_example_edit       ANY      ANY    ANY  /{user}/acme/edit/{id}
acmedemo_example_delete     ANY      ANY    ANY  /{user}/acme/delete/{id}
acmeapi_examples_get        GET      ANY    ANY  /api/acme
acmeapi_examples_edit       POST     ANY    ANY  /api/acme

Problem 1 solved!

Now problem 2, because I want no extra route function or something else. I want to use <a href="{{ path('acmedemo_example_index') }}">Show demos</a> and <a href="{{ path('acmedemo_example_edit', {id: entity.id}) }}">Edit demo</a>.

But if I would use that I would get errors. Also lets do this.

The problem I had with this service is that I have no container >.<

services.yml

parameters:
    router.class: Acme\DemoBundle\Routing\Router
    router.options.generator_base_class: Acme\DemoBundle\Routing\Generator\UrlGenerator

Acme\DemoBundle\Routing\Router

use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
class Router extends BaseRouter implements ContainerAwareInterface
{
    private $container;

    public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null)
    {
        parent::__construct($container, $resource, $options, $context);
        $this->setContainer($container);
    }

    public function getGenerator()
    {
        $generator = parent::getGenerator();
        $generator->setContainer($this->container);
        return $generator;
    }

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
}

Acme\DemoBundle\Routing\Generator\UrlGenerator

use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
class UrlGenerator extends BaseUrlGenerator implements ContainerAwareInterface
{
    private $container;

    protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens)
    {
        /** Set the default user parameter for the routes which haven't a user parameter */
        if(preg_match('/^acmedemo_/i', $name) && in_array('user', $variables) && !array_key_exists('user', $parameters))
        {
            $parameters['user'] = $this->getUser()->getId();
        }

        return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens);
    }

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    /**
     * @see \Symfony\Component\Security\Core\Authentication\Token\TokenInterface::getUser()
     */
    public function getUser()
    {
        if (!$this->container->has('security.context')) {
            throw new \LogicException('The SecurityBundle is not registered in your application.');
        }

        if (null === $token = $this->container->get('security.context')->getToken()) {
            return null;
        }

        if (!is_object($user = $token->getUser())) {
            return null;
        }

        return $user;
    }
}

Problem 2 solved!

(Codes writed by me on Symfony*2.3*)

Thanks for your help. But this is better I think =)

like image 22
PatrickB Avatar answered Oct 20 '22 00:10

PatrickB


With this you have 2 main problems:

PROBLEM 1

The way you have your urls setup you will need to have 2 routes. http://symfony.com/doc/current/book/routing.html#required-and-optional-placeholders

Of course, you can have more than one optional placeholder (e.g. /blog/{slug}/{page}), but everything after an optional placeholder must be optional. For example, /{page}/blog is a valid path, but page will always be required (i.e. simply /blog will not match this route).

Meaning even if you do override how the route is generated when the request comes in for /acme/demos it will not match acmedemo_example_index if it is expecting /{user}/acme/demos even if {user} is optional.

For this you have 2 optional fixes:

FIX 1

Have 2 routes, one to match with the user and one to match with out. both pointing to the same controller action:

acmedemo_example_index            ANY      ANY    ANY  /acme/demos
acmedemo_example_index_with_user  ANY      ANY    ANY  /{user}/acme/demos

FIX 2

Move your optional {user} parameter to the end of the url:

acmedemo_example_index            ANY      ANY    ANY  /acme/demos/{user}

PROBLEM 2

You will need a way to generate the route. For this personally i would create a Twig Function that will basically do what path() does but will append the user.

Take a look at the documentation on how to write a twig extension: http://symfony.com/doc/current/cookbook/templating/twig_extension.html

When registering the extension you will need to pass in some additional services so that you can generate the routes and so you can get the current user.

# src/Acme/DemoBundle/Resources/config/services.yml
services:
    acme.twig.acme_extension:
        class: Acme\DemoBundle\Twig\AcmeExtension
        arguments: ["@security.context","@router"]
        tags:
            - { name: twig.extension }

Then in the extension you will need to use a contructor:

// src/Acme/DemoBundle/Twig/AcmeExtension.php
namespace Acme\DemoBundle\Twig;

class AcmeExtension extends \Twig_Extension
{
    protected $user;
    protected $router;

    public function __construct($security,$router)
    {
        $this->user = $security->getToken()->getUser();
        $this->router = $router;
    }

    /* Declare your function */

    public function acmePath($route,$params,$requirements)
    {
        if(strpos($route,'acmedemo_')===false){
            return $this->router->generate($route,$params,$requirements);
        }
        /** IF YOU USE FIX 1 **/
        array_merge($params,array('user'=>$this->user));
        $newRoute = $route.'_with_user';
        return $this->router->generate($newRoute ,$params,$requirements);

        /** IF YOU USE FIX 2 **/
        array_merge($params,array('user'=>$this->user));
        return $this->router->generate($route,$params,$requirements);
    }
}

Then in your twig files use acmePath() rather than path:

<a href="{{ acmePath('acmedemo_example_index') }}">Show demos</a>

instead of:

<a href="{{ path('acmedemo_example_index') }}">Show demos</a>
like image 41
Chase Avatar answered Oct 19 '22 22:10

Chase