Добавление префикса к роутам в Symfony2

Игорь Адров — Oct 10, 2012    development, symfony2

В очередной раз наткнувшись, казалось бы, на достаточно тривиальную задачу получил достаточно не простое ее решение. Впрочем, возможно, все не так, и я о чем то не знаю. Так что добро пожаловать в комментарии, будет очень интересно услышать предложения.

Задача заключается в том, чтобы добавить ко всем роутам префикс с текущим городом. Т.е. получить ссылки вида “example.com/moscow/news”.

Задачу можно разделить на две. Во-первых, нам нужно сохранять город при переходе по ссылкам. Это не сложно сделать, так как у нас везде используется функция path(). Эта функция использует компонент Routing и к счастью мы можем изменить класс который использует Symfony по-умолчанию.

Редактируем файл config.yml:

parameters:
  router.options.generator_class: App\DefaultBundle\Routing\UrlGenerator
  router.options.generator_base_class: App\DefaultBundle\Routing\UrlGenerator

И создаем свой класс UrlGenerator:

<?php

namespace App\DefaultBundle\Routing;

use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;

/**
 * UrlGenerator generates URL based on a set of routes.
 */
class UrlGenerator extends BaseUrlGenerator
{
    /**
     * @throws Symfony\Component\Routing\Exception\MissingMandatoryParametersException When route has some missing mandatory parameters
     * @throws Symfony\Component\Routing\Exception\InvalidParameterException           When a parameter value is not correct
     */
    protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute)
    {
        if ($city = $this->getContext()->getParameter('_city')) {
            $parameters['_city'] = $city;
        }

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

Здесь нам нужно передать необходимые параметры, которые будут использоваться для генерации ссылок, а так как у нас есть доступ только к самому запросу, поиск города и прочая логика должна происходить в отдельном LocationListener, который выполняется перед каждым запросом. Снова редактируем конфиг и добавляем слушателя:

services:
    app.listener.location:
      class: App\DefaultBundle\Listener\LocationListener
      scope: request
      tags:
        - { name: kernel.event_listener, event: kernel.controller }
      arguments: [@doctrine.orm.entity_manager, @session, @security.context, @router]

LocationListener.php:

<?php

namespace App\DefaultBundle\Listener;

use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class LocationListener
{
    private $em;
    private $session;
    private $securityContext;
    private $router;

    public function __construct($em, $session, $securityContext, $router)
    {
        $this->em = $em;
        $this->session = $session;
        $this->securityContext = $securityContext;
        $this->router = $router;
    }

    public function onKernelController($event)
    {
        if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
            // don't do anything if it's not the master request
            return;
        }

        $request = $event->getRequest();

        $city = null;
        $cityName = $request->get('_city');

        if ($cityName === 'default') {
            $city = $this->em->getRepository('AppGeoBundle:Location')->findDefaultCity();
            $this->router->getContext()->setParameter('_city', $city->getSlug());
        } else {
            $city = $this->em->getRepository('AppGeoBundle:Location')->findOneBySlug($cityName);
            if (!$city) {
                throw new NotFoundHttpException('City not found');
            }
            $this->router->getContext()->setParameter('_city', $cityName);
        }
    }
}

Теперь перейдем ко второй задаче, а именно: нам нужно преобразовать все роуты. При генерации мы должны получить вместо “/news” роут “/{_city}/news”. Решение примерно аналогичное. Переопределяем еще один файл, тот, что отвечает за генерацию списка роутов:

parameters:
  routing.loader.class: App\DefaultBundle\Routing\Loader

Loader.php:

<?php

namespace App\DefaultBundle\Routing;

use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Config\Loader\DelegatingLoader as BaseDelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Routing\RouteCollection;

class Loader extends BaseDelegatingLoader
{
    protected $parser;
    protected $logger;

    /**
     * Constructor.
     *
     * @param ControllerNameParser    $parser   A ControllerNameParser instance
     * @param LoggerInterface         $logger   A LoggerInterface instance
     * @param LoaderResolverInterface $resolver A LoaderResolverInterface instance
     */
    public function __construct(ControllerNameParser $parser, LoggerInterface $logger = null, LoaderResolverInterface $resolver)
    {
        $this->parser = $parser;
        $this->logger = $logger;

        parent::__construct($resolver);
    }

    /**
     * Loads a resource.
     *
     * @param mixed  $resource A resource
     * @param string $type     The resource type
     *
     * @return RouteCollection A RouteCollection instance
     */
    public function load($resource, $type = null)
    {
        $collection = parent::load($resource, $type);

        $defaultCollection = new RouteCollection();

        foreach ($collection->all() as $name => $route) {
            if ($controller = $route->getDefault('_controller')) {
                try {
                    $controller = $this->parser->parse($controller);
                } catch (\Exception $e) {
                    // unable to optimize unknown notation
                }

                $route->setDefault('_controller', $controller);
                $route->setDefault('_city', 'default');

                if ($name[0] == '_') {
                    $defaultCollection->add($name, $route);
                    $collection->remove($name);
                }
            }
        }
        $defaultCollection->addCollection($collection);
        $collection->addPrefix('{_city}');

        return $defaultCollection;
    }
}

Небольшое пояснение: мы добавляем префикс ко всем роутам, кроме тех, что начинаются на подчеркивание. В целом это все, осталось очистить кеш и можно пользоваться.

Все названия классов и функций вымышлены, все совпадения случайны.

Evercode Lab

Close