Conditional resetting email templates in FosUserBundle Symfony Framework - symfony

I have a project in symfony using FosUserBundle and PugxMultiUserBundle because I need 2 user types.
There are CmsUsers and Platform players.
I need a way to have Fos email templates (resetting template,registering template etc.) per user type.
One reset template for CmsUser and another template for Players.
Same thing for Registering.
The problem occurs because these templates are configured in config.yaml
fos_user:
db_driver: orm
firewall_name: api
user_class: PanelBundle\Entity\User
from_email:
address: '%fos_from_address%'
sender_name: '%fos_from_name%'
service:
mailer: api.custom_mailer
user_manager: pugx_user_manager
registration:
confirmation:
enabled: true
template: 'ApiBundle:Email:confirm.email.twig'
resetting:
retry_ttl: 1800 # After how much seconds (30 min) user can request again pass reset
token_ttl: 604800 # After how much seconds (1 week) user token is valid (inactive in user mailbox)
email:
template: 'ApiBundle:Email:resetting.email.twig'
I need a way to config or implement this in conditional way.
If the user type is CmsUser load this template, else load another.
<?php
namespace ApiBundle\Mailer;
use FOS\UserBundle\Mailer\TwigSwiftMailer as BaseMailer;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class CustomUserMailer extends BaseMailer
{
public function __construct(\Swift_Mailer $mailer, UrlGeneratorInterface $router, \Twig_Environment $twig, array $parameters)
{
parent::__construct($mailer, $router, $twig, $parameters);
}
/**
* #param string $templateName
* #param array $context
* #param string $fromEmail
* #param string $toEmail
*/
protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
{
// Create a new mail message.
$message = \Swift_Message::newInstance();
$context['images']['top']['src'] = $message->embed(\Swift_Image::fromPath(
__DIR__.'/../../../web/assets/img/email/header.jpg'
));
$context['images']['bottom']['src'] = $message->embed(\Swift_Image::fromPath(
__DIR__.'/../../../web/assets/img/email/footer.jpg'
));
$context = $this->twig->mergeGlobals($context);
$template = $this->twig->loadTemplate($templateName);
$subject = $template->renderBlock('subject', $context);
$textBody = $template->renderBlock('body_text', $context);
$htmlBody = $template->renderBlock('body_html', $context);
$message->setSubject($subject);
$message->setFrom($fromEmail);
$message->setTo($toEmail);
$message->setBody($htmlBody, 'text/html');
$message->addPart($textBody.'text/plain');
$this->mailer->send($message);
}
}

Hy,
if you can desactivate the FosUserBundle sending emails and create a custom listener who made the same things, get the code of the FosUserBundle listeners and adapt it (ex: add a if($user instanceof CmsUser) etc.) to send custom emails per type.
Check the listeners (EmailConfirmationListener and ResettingListener) in /vendor/friendsofsymfony/user-bundle/EventListener

Related

Symfony 5.4 LDAP and User-Entity Password mixed

Is ist possible to configure user authentification for Symfony 5.4 using either User/Password stored in the User entity oder LDAP depending on a boolean field or the password being null in the User entity?
I need to create some users that have to log on but are not contained in the customers LDAP structure. LDAP is more a comfort thing (single credentials for all apps) here than a security one (no one may logon if not defined in LDAP).
Perhaps I can get around programming the security things from the scatch and just combine two different providers.
Meanwhile I solved it and it was quite easy by using the "normal" password authenticator and modifying a bit of code. The strategy is:
Check if its an LDAP user. If not, use password authentication
Search the user in the LDAP directory
Bail out if not found
Bail out if not unique
Check credentials
The steps I took:
I added a boolean field to the entity USER called ldap_flag
I added variables to .env to specify the LDAP parameters
I modified Security/LoginFormAuthenticator:checkCredentials like this:
if ($user->getLDAPFlag()) {
if ($conn = ldap_connect($_ENV['LDAP_HOST'])) {
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, $_ENV['LDAP_PROTOCOL_VERSION']);
ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
if ($_ENV['LDAP_CERT_CHECK'] == 0)
ldap_set_option($conn, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
$dn = $_ENV['LDAP_BIND_DN'];
$pw = $_ENV['LDAP_BIND_PW'];
if (ldap_bind($conn, $dn, $pw)) {
// Search user
$res = ldap_search($conn, $_ENV['LDAP_SEARCH_DN'], "(&(uid=" . $user->getUserName() . ")(objectClass=inetOrgPerson))", array('dn'));
$entries = ldap_get_entries($conn, $res);
if ($entries["count"] == 1)
return ldap_bind($conn, $entries[0]['dn'], $credentials['password']);
else if ($entries["count"] > 0)
throw new CustomUserMessageAuthenticationException('Benutzer im LDAP nicht eindeutig!');
else
throw new CustomUserMessageAuthenticationException('Benutzer auf dem LDAP Server nicht gefunden!');
} else
// cannot bind
throw new CustomUserMessageAuthenticationException('Kann nicht an LDAP-Server binden!');
ldap_unind($conn);
} else {
// no LDAP Connection
throw new CustomUserMessageAuthenticationException('Keine Verbindung zum LDAP-Server');
}
} else
// internal password-check
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
The error messages are in German but it should be easy to adapt them to an other language as they explain within their context.
I have found another solution, which makes use of Symfony's services. But it is not a one liner. One has to define several configurations, override some services and create two custom classes.
But this advices should be relatively complete.
# config/packages/security.yaml
security:
enable_authenticator_manager: true
providers:
all_users:
chain:
providers: [ldap_users, local_users]
local_users:
entity:
class: App\Entity\User
property: username
ldap_users:
# in services.yml Symfony's provider is overwritten with
# App\Security\LdapUserProvider
ldap:
service: Symfony\Component\Ldap\Ldap # see services.yml
base_dn: '%env(LDAP_BASE_DN)%'
search_dn: '%env(LDAP_SEARCH_DN)%'
search_password: '%env(LDAP_SEARCH_PASSWORD)%'
default_roles: ROLE_USER
uid_key: '%env(LDAP_UID_KEY)%'
firewalls:
main:
pattern: ^/
lazy: true
provider: all_users
form_login_ldap:
check_path: app_login
login_path: app_login
service: Symfony\Component\Ldap\Ldap # see services.yml
dn_string: '%env(LDAP_BASE_DN)%'
search_dn: '%env(LDAP_SEARCH_DN)%'
search_password: '%env(LDAP_SEARCH_PASSWORD)%'
query_string: 'sAMAccountName={username}'
# config/services.yaml
services:
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%env(LDAP_HOST)%'
port: 389
encryption: none
options: { protocol_version: 3, referrals: false, network_timeout: 5 }
# overwrite symfony's LdapUserProvider so that a User entity is used
# instead of the default user class of Symfony.
security.user.provider.ldap:
class: App\Security\LdapUserProvider
arguments: [~, ~, ~, ~, ~, ~, ~, ~, ~]
App\Security\AppCredentialsCheckListener:
decorates: 'security.listener.form_login_ldap.main'
arguments:
$checkLdapCredentialsListener: '#.inner'
$checkCredentialsListener: '#security.listener.check_authenticator_credentials'
// src/Security/LdapUserProvider.php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Ldap\Security\LdapUserProvider as BaseLdapUserProvider;
/**
* This service is responsible for adding a user entity to the local database.
*/
class LdapUserProvider extends BaseLdapUserProvider
{
private EntityManagerInterface $entityManager;
private UserRepository $userRepo;
public function __construct(
LdapInterface $ldap,
string $baseDn,
string $searchDn,
string $searchPassword,
array $defaultRoles,
string $uidKey,
string $filter,
?string $passwordAttribute,
?array $extraFields,
EntityManagerInterface $entityManager,
UserRepository $userRepo
) {
parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
$this->entityManager = $entityManager;
$this->userRepo = $userRepo;
}
protected function loadUser(string $username, Entry $entry)
{
$ldapUser = parent::loadUser($username, $entry);
$user = $this->userRepo->findOneBy(['username' => $ldapUser->getUsername()]);
$flush = false;
if (!$user) {
$user = new User();
$user->setUsername($ldapUser->getUsername());
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
}
// src/Security/AppCredentialsCheckListener.php
namespace App\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
use Symfony\Component\Ldap\Security\LdapBadge;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener;
/**
* This event listener is responsible for checking the password.
* First the LDAP password is checked and as a fallback the local
* password is checked
*/
class AppCredentialsCheckListener implements EventSubscriberInterface
{
private CheckLdapCredentialsListener $checkLdapCredentialsListener;
private CheckCredentialsListener $checkCredentialsListener;
public function __construct(
CheckLdapCredentialsListener $checkLdapCredentialsListener,
CheckCredentialsListener $checkCredentialsListener
) {
$this->checkLdapCredentialsListener = $checkLdapCredentialsListener;
$this->checkCredentialsListener = $checkCredentialsListener;
}
public static function getSubscribedEvents(): array
{
// priority must be higher than the priority of the Symfony listeners
return [CheckPassportEvent::class => ['onCheckPassport', 999]];
}
public function onCheckPassport(CheckPassportEvent $event)
{
try {
// Check ldap password
$this->checkLdapCredentialsListener->onCheckPassport($event);
} catch (BadCredentialsException $e) {
// Fallback to local entity password
$this->checkCredentialsListener->checkPassport($event);
// We have to mark the ldap badge as resolved. Otherwise an exception will be thrown.
/** #var LdapBadge $ldapBadge */
$ldapBadge = $event->getPassport()->getBadge(LdapBadge::class);
$ldapBadge->markResolved();
}
}
}
I have added some comments to the config and the code, which should make clear how it is achieved. I hope it helps anyone.

Sylius / Send Confirmation Email VS state machine

Initial Post:
I use a sylius version 1.0dev.
I need some help about the checkout/complete and email confirmation, the issue seems to be known (https://github.com/Sylius/Sylius/issues/2915), but fixes are not working for me.
At this step, the payment status seems to be paid whereas it should be awaiting_payment.
In addition, the order confirmation email should not be sent, but only after payment on payum gateway.
Is there any existing configuration which triggers this event, and how to implement it please ?
Thanks!
Issue partially solved: I implemented a workaround.
First thing to know: I override shopbundle extending it into my own bundle:
<?php
namespace My\ShopBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MyShopBundle extends Bundle
{
public function getParent()
{
return 'SyliusShopBundle';
}
}
I overrided the sylius.email_manager.order service:
# sylius service configuration
parameters:
# override email manager for order
sylius.myproject.order.email.manager.class:MyBundle\ShopBundle\EmailManager\OrderEmailManager
#################################################
# override service sylius.email_manager.order #
# #
# - use configurable class #
# - send service container in arguments #
# #
#################################################
services:
sylius.email_manager.order:
class: %sylius.myproject.order.email.manager.class%
arguments:
- #sylius.email_sender
- #service_container
Classes look like that:
<?php
namespace My\ShopBundle\EmailManager;
use Sylius\Bundle\CoreBundle\Mailer\Emails;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Mailer\Sender\SenderInterface;
use My\ShopBundle\Mailer\Emails as MyEmails;
/**
* #author I
*/
class OrderEmailManager
{
/**
* #var SenderInterface
*/
protected $emailSender;
/**
*
*/
protected $container ;
/**
* #param SenderInterface $emailSender
*/
public function __construct( SenderInterface $emailSender, $container )
{
$this->emailSender = $emailSender;
$this->container = $container ;
}
/**
* #param OrderInterface $order
* Issue : sent on checkout/complete (payment is not yet done on gateway)
*/
public function sendConfirmationEmail(OrderInterface $order)
{
// sylius.shop.email.bcc is array parameter: expected bcc as array variable
if( $this->container->hasParameter( 'sylius.shop.email.bcc' ) ){
$this->emailSender->send( Emails::ORDER_CONFIRMATION, [$order->getCustomer()->getEmail()], ['order' => $order], $this->container->getParameter( 'sylius.shop.email.bcc' ) ) ;
}else{
// no bcc defined
$this->emailSender->send( Emails::ORDER_CONFIRMATION, [$order->getCustomer()->getEmail()], ['order' => $order] ) ;
}
}
/**
* function used on gateway payment
* here we use MyEmails to define the template (as it is done for the default confimration mail)
*/
public function sendPaymentConfirmationEmail(OrderInterface $order)
{
// sylius.shop.email.bcc is array parameter: expected bcc as array variable
if( $this->container->hasParameter( 'sylius.shop.email.bcc' ) ){
$this->emailSender->send( MyEmails::ORDER_PAID, [$order->getCustomer()->getEmail()], ['order' => $order], $this->container->getParameter( 'sylius.shop.email.bcc' ) ) ;
}else{
// no bcc defined
$this->emailSender->send( MyEmails::ORDER_PAID, [$order->getCustomer()->getEmail()], ['order' => $order] ) ;
}
}
}
/**
*
* (c) I
*
*/
namespace My\ShopBundle\Mailer;
/**
* #author I
*/
class Emails
{
const ORDER_PAID = 'order_paid';
}
Template is located as expected: Resources/views/Email/orderPaid.html.twig and is configured as below:
sylius_mailer:
emails:
order_paid:
subject: "The email subject"
template: "MyShopBundle:Email:orderPaid.html.twig"
To disable default confirmation mailing, configure the state machine:
winzou_state_machine:
# disable order confirmation email on checkout/complete (we prefer on thank you action, see order.yml configuration in MyShopBundle to override the thankYouAction)
sylius_order:
callbacks:
after:
sylius_order_confirmation_email:
disabled: true
To trigger confirmation mailing on thankYou action (use case gateway payment is successfully done), in my bundle (Resources/config/routing/order.yml):
sylius_shop_order_pay:
path: /{lastNewPaymentId}/pay
methods: [GET]
defaults:
_controller: sylius.controller.payum:prepareCaptureAction
_sylius:
redirect:
route: sylius_shop_order_after_pay
sylius_shop_order_after_pay:
path: /after-pay
methods: [GET]
defaults:
_controller: sylius.controller.payum:afterCaptureAction
sylius_shop_order_thank_you:
path: /thank-you
methods: [GET]
defaults:
# OVERRIDE (add: send mail success confirmation + standard controller because this one is not a service and must not be)
_controller: MyShopBundle:Payment:thankYou
_sylius:
template: MyShopBundle:Checkout:thankYou.html.twig
sylius_shop_order_show_details:
path: /{tokenValue}
methods: [GET]
defaults:
_controller: sylius.controller.order:showAction
_sylius:
template: SyliusShopBundle:Checkout:orderDetails.html.twig
grid: sylius_shop_account_order
section: shop_account
repository:
method: findOneBy
arguments:
[tokenValue: $tokenValue]
Finally we override the thankYouAction using a standard symfony controller, as below:
<?php
namespace My\ShopBundle\Controller ;
// use Doctrine\ORM\EntityManager;
// use FOS\RestBundle\View\View;
// use Payum\Core\Registry\RegistryInterface;
// use Sylius\Bundle\ResourceBundle\Controller\RequestConfiguration;
// use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
// use Sylius\Component\Order\Context\CartContextInterface;
// use Sylius\Component\Order\Model\OrderInterface;
// use Sylius\Component\Order\SyliusCartEvents;
// use Sylius\Component\Resource\ResourceActions;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Webmozart\Assert\Assert;
// Carefull using ResourceController extending, cause this is a service and not a controller (different constructor)
class PaymentController extends Controller
{
/**
* #param Request $request
*
* #return Response
*/
public function thankYouAction(Request $request = null)
{
// old implementation (get parameters from custom configuration, more heavy and difficult to maintain)
//$configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
// default value for order
$order = null ;
// if session variable sylius_order_id exist : deal with order
if( $request->getSession()->has( 'sylius_order_id' ) ){
$orderId = $request->getSession()->get( 'sylius_order_id', null ) ;
// old behaviour based on custom configuration in case session variable does not exist anymore: does a homepage redirect
// if (null === $orderId) {
// return $this->redirectHandler->redirectToRoute(
// $configuration,
// $configuration->getParameters()->get('after_failure[route]', 'sylius_shop_homepage', true),
// $configuration->getParameters()->get('after_failure[parameters]', [], true)
// );
// }
$request->getSession()->remove( 'sylius_order_id' ) ;
// prefer call repository service in controller (previously repository came from custom configuration)
$orderRepository = $this->get( 'sylius.repository.order' ) ;
$order = $orderRepository->find( $orderId ) ;
Assert::notNull($order);
// send email confirmation via sylius.email_manager.order service
$this->sendEmailConfirmation( $order ) ;
// old rendering from tankyouAction in Sylius\Bundle\CoreBundle\Controller\OrderController
// $view = View::create()
// ->setData([
// 'order' => $order
// ])
// ->setTemplate($configuration->getParameters()->get('template'))
// ;
// return $this->viewHandler->handle($configuration, $view);
// prefer symfony rendering (controller knows its view, execute a controller creation with command line, your template will be defined inside)
$response = $this->render( 'MyShopBundle:Checkout:thankYou.html.twig', array( 'order' => $order ) ) ;
// deal with http cache expiration duration
$response->setSharedMaxAge( 3600 ) ;
}else{
// redirect to home page
$response = $this->redirect( $this->generateUrl( 'sylius_shop_homepage' ) ) ;
}
return $response ;
}
/**
*
*/
private function sendEmailConfirmation( $order ){
$emailService = $this->container->get( 'sylius.email_manager.order' ) ;
$emailService->sendPaymentConfirmationEmail( $order ) ;
}
}
This is a workaround, issue here is not fully solved, and is about doing same thing using state machine. Default configuration mail seems to be sent on checkout complete whereas it shoud take care about payment status instead.
Thanks !

How to remove Form Field From FOSUserBundle Symfony2.8?

I have install FOSUserBundle in my Symfony project. Now I want to Remove Registration Form Field that by default provide by FOSUserBundle.
Registration Form Fields Are :
User Name
Email Id
Password
Repeat Password
Now I don't want Email Field when User are register so I override Registration form in my bundle.
\\ Front\FrontBundle\Form\RegistrationType.php
<?php
namespace Front\FrontBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->remove('email'); // here I code for remove email field.
}
public function getParent()
{
return 'FOS\UserBundle\Form\Type\RegistrationFormType';
// Or for Symfony < 2.8
// return 'fos_user_registration';
}
public function getBlockPrefix()
{
return 'app_user_registration';
}
// For Symfony 2.x
public function getName()
{
return $this->getBlockPrefix();
}
}
then I change config.yml and services.yml file
\\ App/config/config.yml
fos_user:
db_driver: orm
firewall_name: main
user_class: Front\FrontBundle\Entity\User
registration:
form:
type: Front\FrontBundle\Form\RegistrationType
\\app/config/services.yml
services:
app.form.registration:
class: Front\FrontBundle\Form\RegistrationType
tags:
- { name: form.type, alias: app_user_registration }
So After done with this Email Field remove from my Registration Form but when I submit form after filling username , password , repeat password it's give me any error that The email is not valid.
So I need to change any other file to remove email validation with email field ?
Thanks.
you can remove the field as you have done, but you must make sure that there is no residual server side validation that requires it.
how to do this depends on how you've extended the FOS User class (or if you have).
annotated constraints look like this for instance (from the docs)
class Author
{
/**
* #Assert\NotBlank() <--- remove this
*/
public $name;
}
if youv'e extended the class, you can remove it from your own member definition.
If you've not extended it, extend it and then dont put in the validation constraint.
Or failing everything else (and I think this is hacky), issue a default in the controller before you call isValid().
public function someAction(Request $request) {
// ...
$user = new User();
// fill empty value
$user->setEmail('blank#blank.blank');
// form stuff here
// ...
if ($form->isValid()) {
// do some stuff
}
return $this->render(blahblahbal);
}

Custom i18n routing in 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 !

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