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

Задача заключается в том, чтобы добавить ко всем роутам префикс с текущим городом. Т.е. получить ссылки вида “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:


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:


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:

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

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

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