Custom i18n routing in Symfony - symfony

I'm using JMS\I18nRoutingBundle, Gedmo\Translatable and Gedmo\Sluggable. Routes with default locations works as well, but other locales works without translated slug. My i18n routing have following settings:
# Doctrine extensions
stof_doctrine_extensions:
default_locale: %locale%
translation_fallback: true
orm:
default:
#…
sluggable: true
translatable: true
loggable: false
#…
jms_i18n_routing:
default_locale: cs_CZ
locales: [cs_CZ, en_US]
strategy: custom
hosts:
cs_CZ: example.cz
en_US: example.com
redirect_to_host: true
When I set up route like this:
hw_category:
pattern: /category/{slug}
defaults: { _controller: AcmeSiteBundle:Category:detail }
/**
* #Template
*/
public function detailAction(Category $category)
{}
This routes works
example.cz/category/slug-in-czech
example.com/category/slug-in-czech
But I want to get work example.com/category/slug-in-english which throws 404 exception object not found.

In your controller, you have to override method used in entity repository:
/**
* #Template
* #ParamConverter(
* "category",
* class = "AcmeSiteBundle:Category",
* options = {
* "repository_method" = "findByTranslatedSlug"
* }
* )
*/
public function detailAction(Category $category)
{}
namespace Acme\Bundle\SiteBundle\Entity;
use Doctrine\ORM\EntityRepository;
class CategoryRepository extends EntityRepository
{
public function findByTranslatedSlug($slug)
{
$qb = $this->createQueryBuilder('c')
->where('c.slug = :slug')
->setParameters($slug);
$query = $qb->getQuery();
// set the translation query hint
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
return $query->getOneOrNullResult();
}
}

As i see you are using the ParamConverter to automatically fetch your category.
If slug-in-englishis an existing slug in your database but doctrine refuses to fetch it.
You probably don't have the TranslatableListener added to your EntityManager at that point.
Example:
$translatableListener = new \Gedmo\Translatable\TranslationListener();
$translatableListener->setTranslatableLocale('en_us');
$em->addEventSubscriber($translatableListener);
If you're using StofDoctrineExtensionsBundle:
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
# ...
translatable: true

I had the same issue and as suggested by jkucharovic, you can use the Doctrine ParamConverter to convert your request parameters to an object
To fetch an object from the database, the Doctrine converter uses by default find() method. But since we use Translatable, therefore multiple tables, it is not enough to manage translations, that's why we need to define our own. Here comes findByTranslatedSlug.
/**
* #Template
* #ParamConverter(
* "category",
* class = "AcmeSiteBundle:Category",
* options = {
* "id" = "slug",
* "repository_method" = "findByTranslatedSlug"
* }
* )
*/
public function detailAction(Category $category)
{}
Some details about ParamConverter parameters:
First parameter "category" refers to the name of the method argument (here $category)
"id" option refers to the route placeholder you want to pass(here {slug}) to the customised repository method (findByTranslatedSlug()). Without setting this option, it would throw a PDO exception.
namespace Acme\Bundle\SiteBundle\Entity;
use Doctrine\ORM\EntityRepository;
class CategoryRepository extends EntityRepository
{
public function findByTranslatedSlug($slug)
{
$qb = $this->createQueryBuilder('c')
->where('c.slug = :slug')
->setParameter('slug',$slug);
$query = $qb->getQuery();
// set the translation query hint
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
// If you need to set manually the locale to en_US, you can set this query hint
//$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'en_US');
return $query->getOneOrNullResult();
}
}
I hope this can help
Docs:
Doctrine ParamConverter
Translatable ORM Query Hint

For Symfony > 4
the work around is
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
* #Route("/{slug}", name="your_route_name"))
* #Entity("your_entity_name", expr="repository.findByTranslatedSlug(slug)")
and use the repo method from the accepted answer ;)
Searching for this the whole day, i'm glad i found your post !

Related

Attempted to call an undefined method named "search" of class "FOS\ElasticaBundle\Repository"

I want to use search method from elastica repository but i got error
Attempted to call an undefined method named "search" of class "FOS\ElasticaBundle\Repository".
I use symfony 4 with fos elastica bundle, latest version. In repo i working find method but it don't return any results.
I want do search-as-you-type-autocomplete like here https://www.codevate.com/blog/14-implementing-search-as-you-type-autocomplete-with-elasticsearch-and-symfony
Controller:
/**
* #Route("/search")
*/
public function searchElastic(RepositoryManagerInterface $finder, Request $request){
$searchTerm = $request->query->get('s');
$searchTerm = htmlentities($searchTerm, ENT_QUOTES);
// $finder = $finder->getRepository(\App\Entity\User::class)->find($searchTerm);
// return new response(var_dump($finder[0]->getUsername()));
$completion = new Suggest\Completion('suggest', 'name_suggest');
$completion->setText($searchTerm);
$completion->setFuzzy(array('fuzziness' => 2));
/** var array of App\Entity\User */
$resultSet = $finder->getRepository(\App\Entity\User::class)->search((Query::create($completion)));
var_dump($resultSet);
$suggestions = array();
foreach ($resultSet->getSuggests() as $suggests) {
foreach ($suggests as $suggest) {
foreach ($suggest['options'] as $option) {
$suggestions[] = array(
'id' => $option['_source']['id'],
'username' => $option['_source']['username']
);
}
}
}
return new JsonResponse(array(
'suggestions' => $suggestions,
));
}
Config:
# Read the documentation: https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/setup.md
fos_elastica:
clients:
default: { host: localhost, port: 9200 }
# indexes:
# app: ~
indexes:
app:
client: default
#FOR AUTOCOMPLETE
settings:
index:
analysis:
analyzer:
name_analyzer:
type: custom
tokenizer: standard
filter: [standard, lowercase, asciifolding, elision]
#END FOR AUTOCOMPLETE
types:
user:
properties:
# username: ~
# username:
name_suggest:
# MAPPINGS ADDED FOR AUTOCOMPLETE
type: completion
analyzer: name_analyzer
search_analyzer: name_analyzer
# payloads: true
id:
type: keyword
username:
type: keyword
#MAPPINGS ADDED FOR AUTOCOMPLETE
persistence:
# the driver can be orm, mongodb, phpcr or propel
# listener and finder are not supported by
# propel and should be removed
driver: orm
model: App\Entity\User
provider: ~
listener: ~
finder: ~
User entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Repository\UserRepository;
use Elastica\Search;
/**
* #UniqueEntity(fields="email", message="Email already taken")
* #UniqueEntity(fields="username", message="Username already taken")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #Search(repositoryClass="AppBundle\SearchRepository\PropositionRepository")
*/
class User implements UserInterface
{
Please help, i'm beginner in elastica.
UPADTE:
Now i get this error.
Cannot autowire argument $finder of "App\Controller\DevController::searchElastic2()": it references class "FOS\ElasticaBundle\Doctrine\RepositoryManager" but no such service exists. Try changing the type-hint to "FOS\ElasticaBundle\Manager\RepositoryManagerInterface" instead.
UPDATE:
Argument 1 passed to App\Repository\UserRepository::__construct() must implement interface Symfony\Bridge\Doctrine\RegistryInterface, instance of FOS\ElasticaBundle\Finder\TransformedFinder given, called in C:\xampp\htdocs\projects\symfonysite\vendor\friendsofsymfony\elastica-bundle\src\Manager\RepositoryManager.php on line 101
UPDATE:
UserRepository.php a link
User.php entity a link
Controller a link
**Update: **
I don't get it. Tutorial in link reqire search function which is not in my version, and find function which work for me find only one resuly. Here's simple code which work for me, but find one result.
/**
* #Route("/search")
*/
public function searchElastic(RepositoryManagerInterface $finder, Request $request){
$searchTerm = $request->query->get('s');
$searchTerm = htmlentities($searchTerm, ENT_QUOTES);
$finder = $finder->getRepository(\App\Entity\User::class)->find($searchTerm);
return new response(var_dump($finder[0]->getUsername())); //i tried changing index to 1 but always it return undefined offset
The Search annotation support has been removed the a look to this merged PR , as the repository should be no more accessed through the Bundle:Entity notation, but requesting index/type.
To use the custom repository specify it in the mapping for your user just decalre it under your fos_elastica:
fos_elastica:
...
indexes:
app:
client: default
types:
user:
properties:
# your properties
persistence:
...
repository: App\Repository\UserRepository
Then the custom queries will be available when using the repository returned from the manager:
use FOS\ElasticaBundle\Repository;
class UserRepository extends Repository
{
...
function search(RepositoryManagerInterface $finder, Request $request)
{
/** var FOS\ElasticaBundle\Manager\RepositoryManagerInterface */
$repositoryManager = $finder;
/** var FOS\ElasticaBundle\Repository */
$repository = $repositoryManager->getRepository(AppEntityUser::class);
/** var array of Acme\UserBundle\Entity\User */
$users = $repository->search('bob');
....

Symfony3, override Exception Controller

I try to override Exception Controller with Symfony 3
I followed their example here http://symfony.com/doc/current/controller/error_pages.html but it doesn't work
So first I have created my error page in app/Ressources/TwigBundle.../error404.html.twig
And it works without the controller overriding.
Second, I have added in services.yml, under services:
frontBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
Third in my Controller folder, I have created a CustomExceptionController.php
Inside, I have put (I want to override findtemple() for example)
namespace frontBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
class CustomExceptionController extends ExceptionController
{
/**
* #param Request $request
* #param string $format
* #param int $code An HTTP response status code
* #param bool $showException
*
* #return TemplateReferenceInterface
*/
protected function findTemplate(Request $request, $format, $code,
$showException)
{
}
}
However it doesn't work. This new controller is not taken into consideration and the findTemplate() is not overrided.
Did I miss something?
There are really not a lot of help about that with Symfony 3...
Thank you so much
If you need override the controller try this:
# app/config/services.yml
services:
_defaults:
# ... be sure autowiring is enabled
autowire: true
# ...
AppBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
and:
# app/config/config.yml
twig:
exception_controller: AppBundle:Exception:showException
Link
Regards

sonata admin label on breadcrumb

I have a little problem with sonata admin on Symfony.
I would like to change the default admin label in the breadcrumb:
but I can't find any solution. Can someone help me?
I found this function, but it doesn't work. It looks like that this function is not called.
public function buildBreadcrumbs($action, MenuItemInterface $menu = null) {
$breadCrumb = parent::buildBreadcrumbs($action, $menu);
return $breadCrumb;
}
I use Symfony 2.8.
Try to override classNameLabel property in your admin class:
// in your ProductAdmin class
public function configure()
{
$this->classnameLabel = "Products";
}
The simplest way to achieve what you want is to change translations messages.
If you really want to change the labels you can implement your own label generation strategy.
namespace Blast\CoreBundle\Translator;
use Sonata\AdminBundle\Translator\LabelTranslatorStrategyInterface;
/**
* Class LibrinfoLabelTranslatorStrategy.
*
* Provides a specific label translation strategy for Librinfo.
* It is based on UnderscoreLabelTranslatorStrategy, but without the context,
* and labels are prefixed by "librinfo.label."
*
* i.e. isValid => librinfo.label.is_valid
*/
class LibrinfoLabelTranslatorStrategy implements LabelTranslatorStrategyInterface
{
/**
* {#inheritdoc}
*/
public function getLabel($label, $context = '', $type = '')
{
$label = str_replace('.', '_', $label);
return sprintf('%s.%s.%s', "librinfo", $type, strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $label)));
}
}
define it as a service
blast_core.label.strategy.librinfo:
class: Blast\CoreBundle\Translator\LibrinfoLabelTranslatorStrategy
then pass it to the definition of your admin service like so:
crm.organism:
class: Librinfo\CRMBundle\Admin\OrganismAdmin
arguments: [~, Librinfo\CRMBundle\Entity\Organism, LibrinfoCRMBundle:OrganismAdmin]
tags:
- name: sonata.admin
manager_type: orm
group: Customers Relationship Management
label_translator_strategy: blast_core.label.strategy.librinfo
You will have full control of your admin labels
Also see: SonataAdmin: replace ID in breadcrumbs

symfony set the cookie_domain dynamically

I have a single app that ca serve multiple domains.
I'm having a problem with the framework.session.cookie_domain
I'd like the session to be kept between subdomain, so far so good with cookie_domain set right
Where i have a problem is that i'd like the cookie_domain parameter set dynamically as i don't know in advance which domain the request is coming from.
I tried in the AppKernel.php to do something like :
$domain = substr($_SERVER['HTTP_HOST'], strpos($_SERVER['HTTP_HOST'], '.'));
ini_set('session.cookie_domain', $domain);
But it seems to break my sessions
I could have multiple config.yml one for each domain but i'd like to avoid that.
Do you know a way?
Thanks
I have a similar situation. It's a multi-tenant site with school districts and schools. Each district and school has its own URL as follows:
school-1.district-1.example.com
school-2.district-1.example.com
school-1.district-2.example.com
I want users to be able to access all schools in one district with a single login. I therefore need the cookie to be at the district level.
This is my session storage service.
namespace AppBundle\Services;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
class MySessionStorage extends NativeSessionStorage
{
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null, RequestStack $requestStack)
{
$host = $requestStack->getMasterRequest()->getHost();
$options['cookie_domain'] = substr($host, strpos($host, '.') + 1);
parent::__construct($options, $handler, $metaBag);
}
}
In services.yml
mySessionStorage:
class: AppBundle\Services\MySessionStorage
arguments: [%session.storage.options%, #session.handler, #session.storage.metadata_bag, #request_stack]
In config.yml under framework:
session:
handler_id: session.handler.native_file
storage_id: mySessionStorage
Note that handler_id is null (~) by default in a standard Symfony installation. It needs to be set to something for the service to receive a non-null #session.handler.
That does it for the session cookie but the other one I needed to change is the remember_me cookie. You can set the domain to a constant in config.yml but I need it to depend on host. Maybe I'm missing something but I couldn't see a way to do it dynamically within the security system. RememberMeFactory is directly instantiated, not via configuration. My solution is to listen for kernel.response and replace the cookie before it is sent.
namespace AppBundle\Listeners;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class CookieFix
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
$cookies = $response->headers->getCookies();
$rMe = null;
foreach($cookies as $cookie) {
/** #var \Symfony\Component\HttpFoundation\Cookie $cookie */
if ($cookie->getName() == 'REMEMBERME') {
$rMe = $cookie;
break;
}
}
if ($rMe !== null) {
$host = $this->requestStack->getMasterRequest()->getHost();
$newDomain = substr($host, strpos($host, '.') + 1);
$response->headers->removeCookie($rMe->getName());
$response->headers->setCookie(new Cookie($rMe->getName(), $rMe->getValue(), $rMe->getExpiresTime(), $rMe->getPath(), $newDomain));
}
}
}
I should probably try to get the cookie name from the config.
In services.yml
cookieFix:
class: AppBundle\Listeners\CookieFix
arguments: [#request_stack]
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -100 }
The -100 priority ensures that it runs after the listener that creates the cookie.
Ok, i've figured this out.
It was not that difficult.
I created a custom sessionStorage, extending the default one and i did a simple override where the options were being dealt with: there i calculated my cookie_domain and passed it to the parent::function :
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
/**
* DynamicDomainSessionStorage.
*
* #author Julien Devouassoud
*/
class DynamicDomainSessionStorage extends NativeSessionStorage
{
/**
* setOptions.
*
* {#inheritDoc}
*/
public function setOptions(array $options)
{
if(isset($_SERVER['HTTP_HOST'])){
$domain = substr($_SERVER['HTTP_HOST'], strpos($_SERVER['HTTP_HOST'], '.'));
$options["cookie_domain"] = $domain;
}
return parent::setOptions($options);
}
}
Don't forget:
• to declare your class as a service
• set this service as storage
• set the save_path otherwise cookie_domain seems not to work (breaks the session)
• i set a 'name' as well but i don't think it's essential
• code config.yml :
#...
framework:
#...
session:
storage_id: v3d.session.storage.dynamic_domain
save_path: %kernel.root_dir%/cache/var/sessions
name: SFSESSID
services
v3d.session.storage.dynamic_domain:
class: V3d\Bundle\ApplicationBundle\Services\DynamicDomainSessionStorage

Symfony2 default locale in routing

I have a problem with routing and the internationalization of my site built with Symfony2.
If I define routes in the routing.yml file, like this:
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
It works fine with URLs like:
mysite.com/en/example
mysite.com/fr/example
But doesn't work with
mysite.com/example
Could it be that optional placeholders are permitted only at the end of an URL ?
If yes, what could be a possible solution for displaying an url like :
mysite.com/example
in a default language or redirecting the user to :
mysite.com/defaultlanguage/example
when he visits :
mysite.com/example. ?
I'm trying to figure it out but without success so far.
Thanks.
If someone is interested in, I succeeded to put a prefix on my routing.yml without using other bundles.
So now, thoses URLs work :
www.example.com/
www.example.com//home/
www.example.com/fr/home/
www.example.com/en/home/
Edit your app/config/routing.yml:
ex_example:
resource: "#ExExampleBundle/Resources/config/routing.yml"
prefix: /{_locale}
requirements:
_locale: |fr|en # put a pipe "|" first
Then, in you app/config/parameters.yml, you have to set up a locale
parameters:
locale: en
With this, people can access to your website without enter a specific locale.
You can define multiple patterns like this:
example_default:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index}
requirements:
_locale: fr|en
You should be able to achieve the same sort of thing with annotations:
/**
* #Route("/example", defaults={"_locale"="fr"})
* #Route("/{_locale}/example", requirements={"_locale" = "fr|en"})
*/
Hope that helps!
This is what I use for automatic locale detection and redirection, it works well and doesn't require lengthy routing annotations:
routing.yml
The locale route handles the website's root and then every other controller action is prepended with the locale.
locale:
path: /
defaults: { _controller: AppCoreBundle:Core:locale }
main:
resource: "#AppCoreBundle/Controller"
prefix: /{_locale}
type: annotation
requirements:
_locale: en|fr
CoreController.php
This detects the user's language and redirects to the route of your choice. I use home as a default as that it the most common case.
public function localeAction($route = 'home', $parameters = array())
{
$this->getRequest()->setLocale($this->getRequest()->getPreferredLanguage(array('en', 'fr')));
return $this->redirect($this->generateUrl($route, $parameters));
}
Then, the route annotations can simply be:
/**
* #Route("/", name="home")
*/
public function indexAction(Request $request)
{
// Do stuff
}
Twig
The localeAction can be used to allow the user to change the locale without navigating away from the current page:
{{ targetLanguage }}
Clean & simple!
The Joseph Astrahan's solution of LocalRewriteListener works except for route with params because of $routePath == "/{_locale}".$path)
Ex : $routePath = "/{_locale}/my/route/{foo}" is different of $path = "/{_locale}/my/route/bar"
I had to use UrlMatcher (link to Symfony 2.7 api doc) for matching the actual route with the url.
I change the isLocaleSupported for using browser local code (ex : fr -> fr_FR). I use the browser locale as key and the route locale as value. I have an array like this array(['fr_FR'] => ['fr'], ['en_GB'] => 'en'...) (see the parameters file below for more information)
The changes :
Check if the local given in request is suported. If not, use the default locale.
Try to match the path with the app route collection. If not do nothing (the app throw a 404 if route doesn't exist). If yes, redirect with the right locale in route param.
Here is my code. Works for any route with or without param. This add the locale only when {_local} is set in the route.
Routing file (in my case, the one in app/config)
app:
resource: "#AppBundle/Resources/config/routing.yml"
prefix: /{_locale}/
requirements:
_locale: '%app.locales%'
defaults: { _locale: %locale%}
The parameter in app/config/parameters.yml file
locale: fr
app.locales: fr|gb|it|es
locale_supported:
fr_FR: fr
en_GB: gb
it_IT: it
es_ES: es
services.yml
app.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
LocaleRewriteListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var urlMatcher \Symfony\Component\Routing\Matcher\UrlMatcher;
*/
private $urlMatcher;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'fr', array $supportedLocales, $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
$context = new RequestContext("/");
$this->matcher = new UrlMatcher($this->routeCollection, $context);
}
public function isLocaleSupported($locale)
{
return array_key_exists($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivalent when exists.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
// Do nothing if the route requested has no locale param
$request = $event->getRequest();
$baseUrl = $request->getBaseUrl();
$path = $request->getPathInfo();
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
if ($this->isLocaleSupported($locale)) {
$locale = $this->supportedLocales[$locale];
} else if ($locale == ""){
$locale = $request->getDefaultLocale();
}
$pathLocale = "/".$locale.$path;
//We have to catch the ResourceNotFoundException
try {
//Try to match the path with the local prefix
$this->matcher->match($pathLocale);
$event->setResponse(new RedirectResponse($baseUrl.$pathLocale));
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
}
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Symfony3
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
requirements:
_locale: en|bg| # put a pipe "|" last
There is my Solution, it makes this process faster!
Controller:
/**
* #Route("/change/locale/{current}/{locale}/", name="locale_change")
*/
public function setLocaleAction($current, $locale)
{
$this->get('request')->setLocale($locale);
$referer = str_replace($current,$locale,$this->getRequest()->headers->get('referer'));
return $this->redirect($referer);
}
Twig:
<li {% if app.request.locale == language.locale %} class="selected" {% endif %}>
{{ language.locale }}
</li>
I have a full solution to this that I discovered after some research. My solution assumes that you want every route to have a locale in front of it, even login. This is modified to support Symfony 3, but I believe it will still work in 2.
This version also assumes you want to use the browsers locale as the default locale if they go to a route like /admin, but if they go to /en/admin it will know to use en locale. This is the case for example #2 below.
So for example:
1. User Navigates To -> "/" -> (redirects) -> "/en/"
2. User Navigates To -> "/admin" -> (redirects) -> "/en/admin"
3. User Navigates To -> "/en/admin" -> (no redirects) -> "/en/admin"
In all scenarios the locale will be set correctly how you want it for use throughout your program.
You can view the full solution below which includes how to make it work with login and security, otherwise the Short Version will probably work for you:
Full Version
Symfony 3 Redirect All Routes To Current Locale Version
Short Version
To make it so that case #2 in my examples is possible you need to do so using a httpKernal listner
LocaleRewriteListener.php
<?php
//src/AppBundle/EventListener/LocaleRewriteListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'en', array $supportedLocales = array('en'), $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
}
public function isLocaleSupported($locale)
{
return in_array($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivlent as long as the route will exists when we do so.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
$request = $event->getRequest();
$path = $request->getPathInfo();
$route_exists = false; //by default assume route does not exist.
foreach($this->routeCollection as $routeObject){
$routePath = $routeObject->getPath();
if($routePath == "/{_locale}".$path){
$route_exists = true;
break;
}
}
//If the route does indeed exist then lets redirect there.
if($route_exists == true){
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
//If no locale from browser or locale not in list of known locales supported then set to defaultLocale set in config.yml
if($locale=="" || $this->isLocaleSupported($locale)==false){
$locale = $request->getDefaultLocale();
}
$event->setResponse(new RedirectResponse("/".$locale.$path));
}
//Otherwise do nothing and continue on~
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
To understand how that is working look up the event subscriber interface on symfony documentation.
To activate the listner you need to set it up in your services.yml
services.yml
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
# parameter_name: value
services:
# service_name:
# class: AppBundle\Directory\ClassName
# arguments: ["#another_service_name", "plain_value", "%parameter_name%"]
appBundle.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
Finally this refers to variables that need to be defined in your config.yml
config.yml
# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
app.locales: en|es|zh
locale_supported: ['en','es','zh']
Finally, you need to make sure all your routes start with /{locale} for now on. A sample of this is below in my default controller.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/{_locale}", requirements={"_locale" = "%app.locales%"})
*/
class DefaultController extends Controller
{
/**
* #Route("/", name="home")
*/
public function indexAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
/**
* #Route("/admin", name="admin")
*/
public function adminAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
}
?>
Note the requirements requirements={"_locale" = "%app.locales%"}, this is referencing the config.yml file so you only have to define those requirements in one place for all routes.
Hope this helps someone :)
We created a custom RoutingLoader that adds a localized version to all routes. You inject an array of additional locales ['de', 'fr'] and the Loader adds a route for each additional locale. The main advantage is, that for your default locale, the routes stay the same and no redirect is needed. Another advantage is, that the additionalRoutes are injected, so they can be configured differently for multiple clients/environments, etc. And much less configuration.
partial_data GET ANY ANY /partial/{code}
partial_data.de GET ANY ANY /de/partial/{code}
partial_data.fr GET ANY ANY /fr/partial/{code}
Here is the loader:
<?php
namespace App\Routing;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class I18nRoutingLoader extends Loader
{
const NAME = 'i18n_annotation';
private $projectDir;
private $additionalLocales = [];
public function __construct(string $projectDir, array $additionalLocales)
{
$this->projectDir = $projectDir;
$this->additionalLocales = $additionalLocales;
}
public function load($resource, $type = null)
{
$collection = new RouteCollection();
// Import directly for Symfony < v4
// $originalCollection = $this->import($resource, 'annotation')
$originalCollection = $this->getOriginalRouteCollection($resource);
$collection->addCollection($originalCollection);
foreach ($this->additionalLocales as $locale) {
$this->addI18nRouteCollection($collection, $originalCollection, $locale);
}
return $collection;
}
public function supports($resource, $type = null)
{
return self::NAME === $type;
}
private function getOriginalRouteCollection(string $resource): RouteCollection
{
$resource = realpath(sprintf('%s/src/Controller/%s', $this->projectDir, $resource));
$type = 'annotation';
return $this->import($resource, $type);
}
private function addI18nRouteCollection(RouteCollection $collection, RouteCollection $definedRoutes, string $locale): void
{
foreach ($definedRoutes as $name => $route) {
$collection->add(
$this->getI18nRouteName($name, $locale),
$this->getI18nRoute($route, $name, $locale)
);
}
}
private function getI18nRoute(Route $route, string $name, string $locale): Route
{
$i18nRoute = clone $route;
return $i18nRoute
->setDefault('_locale', $locale)
->setDefault('_canonical_route', $name)
->setPath(sprintf('/%s%s', $locale, $i18nRoute->getPath()));
}
private function getI18nRouteName(string $name, string $locale): string
{
return sprintf('%s.%s', $name, $locale);
}
}
Service definition (SF4)
App\Routing\I18nRoutingLoader:
arguments:
$additionalLocales: "%additional_locales%"
tags: ['routing.loader']
Routing definition
frontend:
resource: ../../src/Controller/Frontend/
type: i18n_annotation #localized routes are added
api:
resource: ../../src/Controller/Api/
type: annotation #default loader, no routes are added
I use annotations, and i will do
/**
* #Route("/{_locale}/example", defaults={"_locale"=""})
* #Route("/example", defaults={"_locale"="en"}, , requirements = {"_locale" = "fr|en|uk"})
*/
But for yml way, try some equivalent...
Maybe I solved this in a reasonably simple way:
example:
path: '/{_locale}{_S}example'
defaults: { _controller: 'AppBundle:Example:index' , _locale="de" , _S: "/" }
requirements:
_S: "/?"
_locale: '|de|en|fr'
Curious about the judgement of the critics ...
Best wishes,
Greg
root:
pattern: /
defaults:
_controller: FrameworkBundle:Redirect:urlRedirect
path: /en
permanent: true
How to configure a redirect to another route without a custom controller
I think you could simply add a route like this:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index }
This way, the locale would be the last locale selected by the user, or the default locale if user locale has not been set. You might also add the "_locale" parameter to the "defaults" in your routing config if you want to set a specific locale for /example:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
I don't know if there's a better way to do this.

Resources