Symfony 4.4 + scheb/2fa : Custom code generator and mailer - php

I'm trying to follow this documentation, but the yaml config throws some errors.
I'm don't know Symfony enough to understand what I am doing wrong.
https://github.com/scheb/2fa/blob/5.x/doc/providers/email.md
services.yaml:
https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
uploads_path: '/uploads/files/'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
# App\Service\CodeGeneratorService:
# arguments: ['#markdown.parser', '#doctrine_cache.providers.my_markdown_cache']
auth_mailer:
# class: Scheb\TwoFactorBundle\Mailer\SymfonyAuthCodeMailer
class: App\Mailer\MyAuthCodeMailer
code_generator_service:
class: App\Service\CodeGeneratorService
# public: true
arguments:
# $mailer: "%scheb_two_factor.security.email.symfony_auth_code_mailer%"
# $mailer: "%scheb_two_factor.security.email.swift_auth_code_mailer%"
# $mailer: App\Mailer\MyAuthCodeMailer
# $mailer: Scheb\TwoFactorBundle\Mailer\SymfonyAuthCodeMailer
# $mailer: Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface
# $mailer: App\service\CodeGeneratorService
# $mailer: "%scheb_two_factor.email.mailer%"
$digits: "%scheb_two_factor.email.digits%"
scheb_2fa.yaml:
# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
security_tokens:
- Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
email:
enabled: true
sender_email: quezako35#gmail.com
sender_name: Quezako # Optional
digits: 12
code_generator: code_generator_service # Use alternative service to generate authentication code
mailer: auth_mailer # Use alternative service to send the authentication code
App\Service\CodeGeneratorService.php:
<?php
declare(strict_types=1);
namespace App\Service;
use Scheb\TwoFactorBundle\Model\PersisterInterface;
use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface;
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Email\Generator\CodeGeneratorInterface;
class CodeGeneratorService implements CodeGeneratorInterface
{
/**
* #var PersisterInterface
*/
private $persister;
/**
* #var AuthCodeMailerInterface
*/
private $mailer;
/**
* #var int
*/
private $digits;
public function __construct(PersisterInterface $persister, AuthCodeMailerInterface $mailer, int $digits)
{
$this->persister = $persister;
$this->mailer = $mailer;
$this->digits = $digits;
}
public function generateAndSend(TwoFactorInterface $user): void
{
$min = 10 ** ($this->digits - 1);
$max = 10 ** $this->digits - 1;
$code = $this->generateCode($min, $max);
$user->setEmailAuthCode((string) $code);
$this->persister->persist($user);
$this->mailer->sendAuthCode($user);
}
public function reSend(TwoFactorInterface $user): void
{
$this->mailer->sendAuthCode($user);
}
protected function generateCode(int $min, int $max): string
{
return substr(md5(rand()), $min, $max);
}
}
The various error messages I get:
Cannot autowire service "code_generator_service": argument "$mailer" of method "App\Service\CodeGeneratorService::__construct()" references interface "Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface" but no such service exists. You should maybe alias this interface to one of these existing services: "App\Mailer\MyAuthCodeMailer", "auth_mailer", "scheb_two_factor.security.email.swift_auth_code_mailer", "scheb_two_factor.security.email.symfony_auth_code_mailer".
You have requested a non-existent parameter "scheb_two_factor.email.mailer". Did you mean one of these: "scheb_two_factor.email.sender_name", "scheb_two_factor.email.template", "scheb_two_factor.email.digits"?
Argument 2 passed to App\Service\CodeGeneratorService::__construct() must implement interface Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface, string given, called in D:\Dev\meet_the_team\var\cache\dev\Container0FXY0Rx\srcApp_KernelDevDebugContainer.php on line 621

The author, Christian Scheb, gave me the answer.
You have to configure a service and reference that service using the # annotation to tell Symfony to use that service instance.
https://github.com/scheb/2fa/issues/22
services:
# ...
auth_mailer:
class: App\Mailer\MyAuthCodeMailer
code_generator_service:
class: App\Service\CodeGeneratorService
arguments:
$mailer: "#auth_mailer"
$digits: "%scheb_two_factor.email.digits%"

Related

Symfony 4 LegacyRouteLoader and LegacyController

I'm trying to implement the LegacyController that is listed on the Symfony documentation https://symfony.com/doc/current/migration.html#legacy-route-loader
Using the custom loader configs for services and routes from:
https://symfony.com/doc/current/routing/custom_route_loader.html#creating-a-custom-loader
I wish there was a complete working example listed somewhere.
When I try to run the debug:router or composer:install I just get an error. This after trying various slight variations on this initial config.
$ console debug:router
In FileLoader.php line 166:
Invalid service id: "App\Legacy\LegacyRouteLoader\" in /var/www/site/config/services.yaml (which is loaded in resource "/var/www/site/config/services.yaml").
In ContainerBuilder.php line 991:
Invalid service id: "App\Legacy\LegacyRouteLoader\"
--
<?php
// src/Legacy/LegacyRouteLoader.php
namespace App\Legacy;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class LegacyRouteLoader extends Loader
{
public function supports($resource, $type = null)
{
return 'extra' === $type;
}
public function load($resource, $type = null)
{
$collection = new RouteCollection();
$finder = new Finder();
$finder->files()->name('*.php');
/** #var SplFileInfo $legacyScriptFile */
foreach ($finder->in($this->webDir) as $legacyScriptFile) {
// This assumes all legacy files use ".php" as extension
$filename = basename($legacyScriptFile->getRelativePathname(), '.php');
$routeName = sprintf('app.legacy.%s', str_replace('/', '__', $filename));
$collection->add($routeName, new Route($legacyScriptFile->getRelativePathname(), [
'_controller' => 'App\Controller\LegacyController::loadLegacyScript',
'requestPath' => '/' . $legacyScriptFile->getRelativePathname(),
'legacyScript' => $legacyScriptFile->getPathname(),
]));
}
return $collection;
}
}
--
<?php
// src/Controller/LegacyController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\StreamedResponse;
class LegacyController
{
public function loadLegacyScript(string $requestPath, string $legacyScript)
{
return StreamedResponse::create(
function () use ($requestPath, $legacyScript) {
$_SERVER['PHP_SELF'] = $requestPath;
$_SERVER['SCRIPT_NAME'] = $requestPath;
$_SERVER['SCRIPT_FILENAME'] = $legacyScript;
chdir(dirname($legacyScript));
include $legacyScript;
}
);
}
}
--
# config/services.yaml
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Legacy\LegacyRouteLoader\:
tags: ['routing.loader']
--
# config/routes.yaml
#index:
# path: /
# controller: App\Controller\DefaultController::index
app_legacy:
resource: .
type: extra
Thanks to #cerad above, I managed to get it going with some changes.
# config/services.yaml
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Legacy\LegacyRouteLoader:
tags: ['routing.loader']
--
<?php
// src/Legacy/LegacyRouteLoader.php
namespace App\Legacy;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class LegacyRouteLoader extends Loader
{
private $webDir = __DIR__.'/../../public/';
public function supports($resource, $type = null)
{
return 'extra' === $type;
}
public function load($resource, $type = null)
{
$collection = new RouteCollection();
$finder = new Finder();
$finder->files()->name('*.php');
/** #var SplFileInfo $legacyScriptFile */
foreach ($finder->in($this->webDir) as $legacyScriptFile) {
// This assumes all legacy files use ".php" as extension
$filename = basename($legacyScriptFile->getRelativePathname(), '.php');
$routeName = sprintf('app.legacy.%s', str_replace('/', '__', $filename));
$collection->add($routeName, new Route($legacyScriptFile->getRelativePathname(), [
'_controller' => 'App\Controller\LegacyController::loadLegacyScript',
'requestPath' => '/' . $legacyScriptFile->getRelativePathname(),
'legacyScript' => $legacyScriptFile->getPathname(),
]));
}
return $collection;
}
}

How can I call a environnement variable declared in my service.yml?

I'm working on a symfony project that need to be refactorized.
I'm trying to call a environnement variable declared in servces.yml
When i'm running php bin/console debug:container --parameters I can see the variables.
I tried $this->getParameters('param_name');
When I'm dumping them in a controller it's working fine but when i'm trying to access them from a service it is failing -> Method 'getParameter' not found in 'TwitterService'
I thought that this method was native in symfony.
Here is my twitter service
```namespace App\Services;
use Abraham\TwitterOAuth\TwitterOAuth;
/**
* Class TwitterService.
*/
class TwitterService
{
/** #const int NUMBER_OF_TWEETS */
private const NUMBER_OF_TWEETS = 2;
/**
* #param string $twitterID
*
* #return array|object
*/
public function getTwitterFeed($twitterID = 'RISKandMe')
{
// #todo move config to .env
$consumerKey = $this->getParameter('consumer_key');
$consumerSecret = $this->getParameter('consumer_secret');
$accessToken = $this->getParameter('access_token');
$accessTokenSecret = $this->getParameter('access_token');
// Authenticate with twitter
$twitterConnection = new TwitterOAuth(
$consumerKey,
$consumerSecret,
$accessToken,
$accessTokenSecret
);
// Get the user timeline feeds
return $twitterConnection->get(
'statuses/user_timeline',
[
'screen_name' => $twitterID,
'count' => self::NUMBER_OF_TWEETS,
'exclude_replies' => false,
]
);
}
}
Here is my services.yml:
parameters:
locale: 'fr'
api_key_validity: '%env(int:APIKEYVALIDITY)%'
activation_delay: '%env(int:ACTIVATIONDELAY)%'
gophish_api_key: '%env(GOPHISHAPIKEY)%'
gophish_server: '%env(GOPHISHSERVER)%'
gophish_white_list: '%env(GOPHISHWHITELIST)%'
mailjet_public: '%env(MAILJET_PUBLIC_KEY)%'
mailjet_secret: '%env(MAILJET_PRIVATE_KEY)%'
sender_address: '%env(SENDER_ADDRESS)%'
sender_name: '%env(SENDER_NAME)%'
consumer_key: '%env(CONSUMER_KEY)%'
consumer_secret: '%env(CONSUMER_SECRET)%'
access_token: '%env(ACCESS_TOKEN)%'
access_token_secret: '%env(ACCESS_TOKEN_SECRET)%'
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# user_listener:
# class: App\EntityListener\UserListener
# arguments: ['#logger']
# tags:
# - { name: doctrine.orm.entity_listener,entity: App\Entity\User}
#
form_authenticator:
class: App\Security\FormAuthenticator
arguments: ["#logger","#router","#security.password_encoder","#doctrine.orm.default_entity_manager",CsrfTokenManagerInterface]
token_authenticator:
class: App\Security\TokenAuthenticator
arguments: ["#logger","#router","#security.password_encoder","#doctrine.orm.default_entity_manager",CsrfTokenManagerInterface,ParameterBagInterface]
DbUserProvider:
class: App\Security\DbUserProvider
FickleUserProvider:
class: App\Security\FickleUserProvider
BackEndUserProvider:
class: App\Security\BackEndUserProvider
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
App\Controller\Services\Helper:
arguments: ['#logger','#router','#security.password_encoder','#mailer','#session','#doctrine.orm.default_entity_manager']
tags: ['controller.service_arguments']
App\Controller\Services\GoPhish:
arguments: []
tags: []
App\Controller\:
resource: '../src/Controller/DashboardController.php'
arguments: ["#logger","#router","#security.password_encoder",'#doctrine.orm.default_entity_manager',App\Controller\Services\Helper]
tags: ['controller.service_arguments']
twig.extension.intl:
class: Twig_Extensions_Extension_Intl
tags:
- { name: twig.extension }
# session.listener:
# class: App\Listeners\SessionListener
# tags:
# - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
# user.activity_listener:
# class: App\Listeners\ActivityListener
# tags:
# - { name: kernel.event_listener, event: kernel.controller, method: onCoreController }
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- '%env(string:DBCONNEXIONPARAMETERS)%'
- { db_table: 'ht_session', db_username: '%env(string:DBUSER)%', db_password: '%env(string:DBPASSWORD)%'}
app.services.twitter:
class: App\Services\TwitterService
If someone has any idea they would be more than appreciated.
Thank you very much for you attention.
The $this->getParameter() helper function is only available in controllers extending from the class Symfony\Bundle\FrameworkBundle\Controller\AbstractController;.
In order to access a container-parameter in your service you need to inject or autowire it into your service.
# services.yml
services:
_defaults:
bind:
# schema -> [constructor-variable name]: [parameter name]
$consumerSecret: '%consumer_secret%'
$consumerKey: '%consumer_key%'
# [..]
Then use them as follows:
class TwitterService
{
private $csonsumerSecret;
private $consumerKey;
public function __construct(
string $consumerKey,
string $consumerSecret,
// [..]
) {
$this->consumerKey = $consumerKey;
$this->consumerSecret = $consumerSecret;
// [..]
}
public function getTwitterFeed($twitterID = 'RISKandMe')
{
$twitterConnection = new TwitterOAuth(
$this->consumerKey,
$this->consumerSecret,
// [..]
);

Multiple region caches with Doctrine 2 second level cache and Symfony 3.3

Have a distributed SF3.3 application running on multiple AWS EC2 instances with a central ElastiCache (redis) cluster.
Each EC2 instance also runs a local Redis instance which is used for Doctrine meta and query caching.
This application utilises Doctrines Second Level Cache, which works very well from a functional point of view. But performance is poor (900-1200ms page loads) on AWS due to the 400+ cache calls it makes to load in our Country and VatRate entities required on many of our pages.
As these Country and VatRate entities change rarely I'd like to utilise both the local Redis instance and ElastiCache for result caching by using different regions defined in the second level cache. This should reduce the latency problem with the 400+ cache calls as when running on a single box page loads are sub 100ms. Reading the documentation this all seems to be possible, just not entirely sure how to configure it with Symfony and PHP-Cache.
An example of the current configuration:
app/config/config.yml
doctrine:
dbal:
# .. params
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
second_level_cache:
enabled: true
region_cache_driver:
type: service
id: doctrine.orm.default_result_cache
cache_adapter:
providers:
meta: # Used for version specific
factory: 'cache.factory.redis'
options:
host: 'localhost'
port: '%redis_local.port%'
pool_namespace: "meta_%hash%"
result: # Used for result data
factory: 'cache.factory.redis'
options:
host: '%redis_result.host%'
port: '%redis_result.port%'
pool_namespace: result
cache:
doctrine:
enabled: true
use_tagging: true
metadata:
service_id: 'cache.provider.meta'
entity_managers: [ default ]
query:
service_id: 'cache.provider.meta'
entity_managers: [ default ]
result:
service_id: 'cache.provider.result'
entity_managers: [ default ]
src/AppBundle/Entity/Country.php
/**
* #ORM\Table(name = "countries")
* #ORM\Cache(usage = "READ_ONLY")
*/
class Country
{
// ...
/**
* #var VatRate
*
* #ORM\OneToMany(targetEntity = "VatRate", mappedBy = "country")
* #ORM\Cache("NONSTRICT_READ_WRITE")
*/
private $vatRates;
// ...
}
src/AppBundle/Entity/VatRate.php
/**
* #ORM\Table(name = "vatRates")
* #ORM\Cache(usage = "READ_ONLY")
*/
class VatRate
{
// ...
/**
* #var Country
*
* #ORM\ManyToOne(targetEntity = "Country", inversedBy = "vatRates")
* #ORM\JoinColumn(name = "countryId", referencedColumnName = "countryId")
*/
private $country;
// ...
}
src/AppBundle/Entity/Order.php
/**
* #ORM\Table(name = "orders")
* #ORM\Cache(usage = "NONSTRICT_READ_WRITE")
*/
class Order
{
// ...
/**
* #var Country
*
* #ORM\ManyToOne(targetEntity = "Country")
* #ORM\JoinColumn(name = "countryId", referencedColumnName = "countryId")
*/
private $country;
// ...
}
Attempted Configuration
app/config/config.yml
doctrine:
dbal:
# .. params
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
second_level_cache:
enabled: true
region_cache_driver: array
regions:
local:
type: service
service: "doctrine.orm.default_result_cache" # TODO: needs to be local redis
remote:
type: service
service: "doctrine.orm.default_result_cache" # TODO: needs to be remote redis
cache_adapter:
providers:
meta: # Used for version specific
factory: 'cache.factory.redis'
options:
host: 'localhost'
port: '%redis_local.port%'
pool_namespace: "meta_%hash%"
result: # Used for result data
factory: 'cache.factory.redis'
options:
host: '%redis_result.host%'
port: '%redis_result.port%'
pool_namespace: result
cache:
doctrine:
enabled: true
use_tagging: true
metadata:
service_id: 'cache.provider.meta'
entity_managers: [ default ]
query:
service_id: 'cache.provider.meta'
entity_managers: [ default ]
result:
service_id: 'cache.provider.result'
entity_managers: [ default ]
src/AppBundle/Entity/Country.php
/**
* #ORM\Table(name = "countries")
* #ORM\Cache(usage = "READ_ONLY", region = "local")
*/
class Country
{
// as above
}
src/AppBundle/Entity/VatRate.php
/**
* #ORM\Table(name = "vatRates")
* #ORM\Cache(usage = "READ_ONLY", region = "local")
*/
class VatRate
{
// as above
}
src/AppBundle/Entity/Order.php
/**
* #ORM\Table(name = "orders")
* #ORM\Cache(usage = "NONSTRICT_READ_WRITE", region = "remote")
*/
class Order
{
// as above
}
Which results in
Type error: Argument 1 passed to Doctrine\ORM\Cache\DefaultCacheFactory::setRegion() must be an instance of Doctrine\ORM\Cache\Region, instance of Cache\Bridge\Doctrine\DoctrineCacheBridge given,
Not too sure where to go from here, been working from the tests here: https://github.com/doctrine/DoctrineBundle/blob/74b408d0b6b06b9758a4d29116d42f5bfd83daf0/Tests/DependencyInjection/Fixtures/config/yml/orm_second_level_cache.yml but the lack of documentation for configuring this makes it a little more challenging!
After much playing around with the PHP-Cache library, it's clear from looking in the CacheBundle compiler that it will only ever support one DoctrineBridge instance from the configuration. https://github.com/php-cache/cache-bundle/blob/master/src/DependencyInjection/Compiler/DoctrineCompilerPass.php
Solution was to create my own compiler, not pretty but it seems to work.
src/AppBundle/DependencyInjection/Compiler/DoctrineCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Cache\Bridge\Doctrine\DoctrineCacheBridge;
use Cache\CacheBundle\Factory\DoctrineBridgeFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class DoctrineCompilerPass implements CompilerPassInterface
{
/** #var ContainerBuilder */
private $container;
public function process(ContainerBuilder $container)
{
$this->container = $container;
$this->enableDoctrineCache('local');
$this->enableDoctrineCache('remote');
}
private function enableDoctrineCache(string $configName)
{
$typeConfig = [
'entity_managers' => [
'default'
],
'use_tagging' => true,
'service_id' => 'cache.provider.' . $configName
];
$bridgeServiceId = sprintf('cache.service.doctrine.%s.entity_managers.bridge', $configName);
$this->container->register($bridgeServiceId, DoctrineCacheBridge::class)
->setFactory([DoctrineBridgeFactory::class, 'get'])
->addArgument(new Reference($typeConfig['service_id']))
->addArgument($typeConfig)
->addArgument(['doctrine', $configName]);
}
}
src/AppBundle/AppBundle.php
use AppBundle\DependencyInjection\Compiler\DoctrineCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new DoctrineCompilerPass());
}
}
app/config/config.yml
doctrine:
dbal:
# ... params
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
second_level_cache:
enabled: true
regions:
remote:
cache_driver:
type: service
id: cache.service.doctrine.remote.entity_managers.bridge
local:
cache_driver:
type: service
id: cache.service.doctrine.local.entity_managers.bridge
cache_adapter:
providers:
local:
factory: 'cache.factory.redis'
options:
host: '%redis_local.host%'
port: '%redis_local.port%'
pool_namespace: "local_%hash%"
remote:
factory: 'cache.factory.redis'
options:
host: '%redis_result.host%'
port: '%redis_result.port%'
pool_namespace: 'result'
cache:
doctrine:
enabled: true
use_tagging: true
metadata:
service_id: 'cache.provider.local'
entity_managers: [ default ]
query:
service_id: 'cache.provider.local'
entity_managers: [ default ]
While this seems to work to some extent, there's some inconsistencies local cache calls resulting in 500 errors when theres probably something missing in the cache. Overall think I'm trying to bend the second level cache more than it was designed to.
The error message you are getting entirely reflects the root of your issue. You are passing DoctrineCacheBridge instances (the underlying class of doctrine.orm.default_result_cache) when instances of the Doctrine\ORM\Cache\Region interface expected:
second_level_cache:
#...
regions:
local:
type: service
service: "region_service_not_cache_service" # Here is a Region instance expected
remote:
type: service
service: "region_service_not_cache_service" #Here is a Region instance expected
In your former configuration the doctrine.orm.default_result_cache cache service is set as the default cache through the region_cache_driver setting. \Doctrine\ORM\Cache\DefaultCacheFactory generates instances of DefaultRegion on flight (as none was preconfigured) and feeds the default cache to them.
The latter configuration is expected to have pre-configured regions and could be fixed several ways. I suggest the next:
dbal:
# .. params
orm:
#...
second_level_cache:
#...
regions:
local:
type: default
cache_driver:
type: service
id: "doctrine.orm.default_query_cache" # NOTE that this is the service id of your local cache generated by PHP-Cache Bundle
remote:
type: default
cache_driver:
type: service
id: "doctrine.orm.default_result_cache" # NOTE that this is the service id of your remote cache generated by PHP-Cache Bundle
Here you tell Doctrine to create 2 DefaultRegion regions under the local and remote keys and pass local_cache and remote_cache to them correspondingly.
And it's better to return region_cache_driver to the former value otherwise DefaultRegions generated on flight will use array cache:
second_level_cache:
enabled: true
region_cache_driver:
type: service
id: doctrine.orm.default_result_cache

No property defined for entity for resource owner

So i'm trying to use the HWI-OAuthBundle and FoS-UserBundle in order to enable users to connect from our schools credentials. So I applied what was said in the docs and configured the custom resource owner.
When i try to login, i'm correctly redirected to the schools OAuth service where i login & authorize the client to access my profile but then i have this error message :
No property defined for entity for resource owner 'myecp'.
I tried several solutions I found on internet, none of them works for me.
Here is my code :
config.yml
#HWIOAuthBundle
hwi_oauth:
connect:
account_connector: my.myecp.user_provider
firewall_names: [secured_area]
fosub:
username_iterations: 30
properties:
myecp: myecp_id
resource_owners:
myecp:
type: oauth2
client_id: "%myecp_client_id%"
client_secret: "%myecp_secret%"
access_token_url: https://my.ecp.fr/oauth/v2/token
authorization_url: https://my.ecp.fr/oauth/v2/auth
infos_url: https://my.ecp.fr/api/v1/members/me
user_response_class: HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse
paths:
identifier: id
nickname: login
realname: [first_name, last_name]
mail: mail
options:
csrf: true
#FOSUserBundle
fos_user:
db_driver: orm
firewall_name: main
user_class: AppBundle\Entity\Personnes
from_email:
address: "%mailer_user%"
sender_name: "%mailer_user%"
#Services
services:
my.myecp.user_provider:
class: AppBundle\Security\Core\User\FOSUBPersonnesProvider
arguments: ['#fos_user.user_manager', { myecp: myecp_id }]
security.yml :
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/security.html
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
# http://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
providers:
fos_userbundle:
id: fos_user.user_provider.username
# in_memory:
# memory: ~
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
anonymous: ~
oauth:
resource_owners:
myecp: "/login/check"
login_path: /login
use_forward: false
failure_path: /login
oauth_user_provider:
service: my.oauth_aware.user_provider.service
main:
# anonymous: ~
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
services.yml:
services:
# service_name:
# class: AppBundle\Directory\ClassName
# arguments: ["#another_service_name", "plain_value", "%parameter_name%"]
my.oauth_aware.user_provider.service:
class: HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider
arguments:
- '#fos_user.user_manager'
- ['pass properties as array']
routing.yml:
app:
resource: "#AppBundle/Controller/"
type: annotation
hwi_oauth_redirect:
resource: "#HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /connect
hwi_oauth_connect:
resource: "#HWIOAuthBundle/Resources/config/routing/connect.xml"
prefix: /connect
hwi_oauth_login:
resource: "#HWIOAuthBundle/Resources/config/routing/login.xml"
prefix: /login
fos_user:
resource: "#FOSUserBundle/Resources/config/routing/all.xml"
myecp_login:
path: /login/check
logout:
path: /logout
FOSUBPersonnesProvider.php
class FOSUBPersonnesProvider extends BaseFOSUBProvider
{
/**
* {#inheritDoc}
*/
public function connect(UserInterface $user, UserResponseInterface $response)
{
// get property from provider configuration by provider name
// , it will return `myecp_id` in that case (see service definition below)
$property = $this->getProperty($response);
$username = $response->getUsername(); // get the unique user identifier
//on connect - get the access token and the user ID
$service = $response->getResourceOwner()->getName();
$setter = 'set'.ucfirst($service);
$setter_id = $setter.'Id';
$setter_token = $setter.'AccessToken';
//we "disconnect" previously connected users
$existingUser = $this->userManager->findUserBy(array($property => $username));
if (null !== $existingUser) {
$existingUser->$setter_id(null);
$existingUser->$setter_token(null);
$this->userManager->updateUser($existingUser);
}
$user->$setter_id($username);
$user->$setter_token($response->getAccessToken());
$this->userManager->updateUser($user);
}
/**
* {#inheritdoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$userId = $response->getUsername();
$user = $this->userManager->findUserBy(array('myecpId' => $userId));
// if null just create new user and set it properties
if (null === $user) {
$first_name = $response->getFirstName();
$last_name = $response->getLastName();
$email = $response->getEmail();
$service = $response->getResourceOwner()->getName();
$setter = 'set'.ucfirst($service);
$setter_id = $setter.'Id';
$setter_token = $setter.'AccessToken';
// create new user here
$user = $this->userManager->createUser();
$user->setPrenom($first_name);
$user->setNom($last_name);
$user->setMail($email);
$user->$setter_id($userId);
$user->$setter_token($response->getAccessToken());
$this->userManager->updateUser($user);
return $user;
}
// else update access token of existing user
$user = parent::loadUserByOAuthUserResponse($response);
$serviceName = $response->getResourceOwner()->getName();
$setter = 'set' . ucfirst($serviceName) . 'AccessToken';
$user->$setter($response->getAccessToken());//update access token
return $user;
}
}
Thanks for your help !
First change in file services.yml line where You pass argument for user.provider.
For example change last line, below is my working example for google.
my.custom.user_provider:
class: YOURBUNDLENAME\Security\Core\MyFOSUBUserProvider
arguments:
- '#fos_user.user_manager'
- arguments: **[ #fos_user.user_manager, { google: googleID }** ]
Next change $property to name of User entity property name ( myecp ?? ) . In my exmaple its googleID. $property is in FOSUBPersonnesProvider.php below connect() method.
My User.php entity looks like:
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="google_id", type="string", nullable=true)
*/
private $googleID;
I hope this will help you
I solved this problem.
Because here passing arguments as array:
arguments: ['#fos_user.user_manager', { myecp: myecp_id }] //In Service
Change to :
arguments: ['#fos_user.user_manager', myecp: myecp_id ]
When send second parameter in FOSUserProvider merging array HERE
Then ERROR message showing HERE
protected function getProperty(UserResponseInterface $response)
{
$resourceOwnerName = $response->getResourceOwner()->getName();
if (!isset($this->properties[$resourceOwnerName])) {
throw new \RuntimeException(sprintf("No property defined for entity for resource owner '%s'.", $resourceOwnerName));
}
return $this->properties[$resourceOwnerName];
}
If you are using Facebook:
arguments: ['#fos_user.user_manager', facebook: facebook ]
If you are using amazon:
arguments: ['#fos_user.user_manager', amazon: amazon ]
If you are using Odnoklassniki:
arguments: ['#fos_user.user_manager', odnoklassniki: odnoklassniki ]
If you are using github:
arguments: ['#fos_user.user_manager', github: github ]
If you are using google:
arguments: ['#fos_user.user_manager', google: google ]

Symfony 3 Redirect All Routes To Current Locale Version

I am working on a symfony application where my goal is no matter what page the user is on it will navigate to the locale version of the page.
For example, if the user navigates to "/" the home page, it will redirect to "/en/"
If they are on "/admin" page it will redirect to "/en/admin", in such a way that the _locale property is set from the route.
Also it needs to determine the locale if they visit /admin from the users browser since no locale was determined so it knows which page to redirect to.
Currently my default controller looks like below since I'm testing. I'm using the dev mode & profiler to test that translations are working in correct.
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
* #Route("/{_locale}/", name="homepage_locale")
*/
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
]);
}
}
This current method will keep the user at "/" if they navigate there, but I want to have it redirect to "/en/". This should work for other pages too, like /admin, or /somepath/pathagain/article1 (/en/admin , /en/somepath/pathagain/article1)
How would I do this?
References I've read that did not help:
Symfony2 Use default locale in routing (one URL for one language)
Symfony2 default locale in routing
::Update::
I have not solved my issue but I've come close as well as learned a few tricks to be more efficient.
DefaultController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="home", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
* #Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
*/
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", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
* #Route("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
*/
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
]);
}
}
?>
Config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.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
framework:
#esi: ~
translator: { fallbacks: ["%locale%"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
#serializer: { enable_annotations: true }
templating:
engines: ['twig']
#assets_version: SomeVersionScheme
default_locale: "%locale%"
trusted_hosts: ~
trusted_proxies: ~
session:
# handler_id set to null will use default session handler from php.ini
handler_id: ~
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
fragments: ~
http_method_override: true
assets: ~
# Twig Configuration
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
# path: "%database_path%"
orm:
auto_generate_proxy_classes: "%kernel.debug%"
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool: { type: memory }
Notice under parameters the value app.locales: en|es|zh. This is now a value I can reference whenever I create my routes if I plan to support more locales in the future which I do. Those routes are english, spanish, chinese in that order for those curious. In the DefaultController in the annotations the "%app.locales%" is the part that references the config parameter.
The problem with my current method is going to /admin for example does not redirect the user to /{browsers locale}/admin, which would be the more elegant solution to keep everything organized... but at least the routes work. Still looking for better solution.
****Update****
I think I may have possibly found the answer here as the bottom answer given (Add locale and requirements to all routes - Symfony2), the answer by Athlan. Just not sure how to implement this in symfony 3 as his directions were not clear enough to me.
I think this article might help also (http://symfony.com/doc/current/components/event_dispatcher/introduction.html)
I don't have enough reputation to add a comment to the correct solution. So I'm adding a new answer
You can add "prefix: /{_locale}" at app/config/routing.yml like this:
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
So you don't need to add it to every route to every action. For the following steps. Thank you very much it's perfect.
After 12 hours of looking into this I finally found an acceptable solution. Please post revised versions of this solution if you can make it more efficient.
Some things to note, my solution is particular to my need. What it does is force any URL to go to a localized version if it exists.
This requires some conventions to be followed when you create routes.
DefaultController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
*/
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("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
*/
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
]);
}
}
?>
Notice that both routes always start with "/{_locale}/". For this to work every route in your project needs to have this. You just put the real route name afterwards. For me I was okay with this scenario. You can modify my solution to fit your needs easily enough.
The first step is to create a listen on the httpKernal to intercept requests before they go to the routers to render them.
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)),
);
}
}
Finally you set the services.yml to start the listener up.
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 }
Also in the config.yml you will want to add the following under parameters:
config.yml
parameters:
locale: en
app.locales: en|es|zh
locale_supported: ['en','es','zh']
I wanted there to be only one place you define the locales but I wound up having to do 2...but at least they are in the same spot so easy to change.
app.locales is used in default controller (requirements={"_locale" = "%app.locales%"}) and locale_supported is used in the LocaleRewriteListener. If it detects a locale that is not in the list it will fallback to the default locale, which in this case is the value of locale:en.
app.locales is nice with the requirements command because it will cause a 404 for any locales that do not match.
If you are using forms and have a login you will need to do the following to your security.yml
Security.yml
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12
AppBundle\Entity\User:
algorithm: bcrypt
cost: 12
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
database:
entity: { class: AppBundle:User }
#property: username
# if you're using multiple entity managers
# manager_name: customer
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
form_login:
check_path: login_check
login_path: login_route
provider: database
csrf_token_generator: security.csrf.token_manager
remember_me:
secret: '%secret%'
lifetime: 604800 # 1 week in seconds
path: /
httponly: false
#httponly false does make this vulnerable in XSS attack, but I will make sure that is not possible.
logout:
path: /logout
target: /
access_control:
# require ROLE_ADMIN for /admin*
#- { path: ^/login, roles: ROLE_ADMIN }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/(.*?)/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
The important change to note here is that (.*?)/login will authenticate anonymously so your users can still login. This does mean that routes like..dogdoghere/login could trigger, but the requirements I will show you shortly on the login routes prevent this and will throw 404 errors. I like this solution with the (.*?) versus [a-z]{2} incase you wanted to use en_US type locales.
SecurityController.php
<?php
// src/AppBundle/Controller/SecurityController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* #Route("{_locale}/login", name="login_route", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
*/
public function loginAction(Request $request)
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'security/login.html.twig',
array(
// last username entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
/**
* #Route("/{_locale}/login_check", name="login_check", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
*/
public function loginCheckAction()
{
// this controller will not be executed,
// as the route is handled by the Security system
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
?>
Note that even these paths use {_locale} in front. I like this however so I can give custom logins for different locales. Just keep that in mind. The only route that does not need the locale is logout which works just fine since its really only an intercept route for the security system. Also notice it uses the requirements which is set from the config.yml, so you only have to edit it in one place for all the routes across your projects.
Hope this helps someone trying to do what I was doing!
NOTE:: To test this easily I use 'Quick Language Switcher' extension for Google Chrome, which changes the accept-language header on all requests.
final function smallResumeOfResearching($localeRewrite, $opinion = 'IMHO') :)
The method, provided by mr. Joseph working great with routes like /{route_name}, or /, but not with routes like /article/slug/other.
If we use modified mr.Joseph's method, provided by https://stackoverflow.com/a/37168304/9451542, we will lost profiler and debugger in dev mode.
If we want more flexible solution, onKernelRequest method can be modified like this (thanks to mr. Joseph, thanks to https://stackoverflow.com/a/37168304/9451542):
public function onKernelRequest(GetResponseEvent $event)
{
$pathInfo = $event->getRequest()->getPathinfo();
$baseUrl = $event->getRequest()->getBaseUrl();
$checkLocale = explode('/', ltrim($pathInfo, '/'))[0];
//Or some other logic to detect/provide locale
if (($this->isLocaleSupported($checkLocale) == false) && ($this->defaultLocale !== $checkLocale)) {
if ($this->isProfilerRoute($checkLocale) == false) {
$locale = $this->defaultLocale;
$event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
}
/* Or with matcher:
try {
//Try to match the path with the locale prefix
$this->matcher->match('/' . $locale . $pathInfo);
//$event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
}
*/
}
}
note: $this->profilerRoutes = array('_profiler', '_wdt', '_error');
Thanks to Susana Santos for pointing to simple config method :)
Small improvement for Symfony 3.4:
Be sure, that the getSubscribedEvents() will register LocaleRewriteListener BEFORE RouterListener::onKernelRequest and BEFORE LocaleListener::onKernelRequest. Integer 17 must be greater than RouterListener::onKernelRequest priotity. Otherwise you will got 404.
bin/console debug:event-dispatcher
Service definition in services.yml must be (depends on Symfony configuration):
AppBundle\EventListener\LocaleRewriteListener:
arguments: ['#router', '%kernel.default_locale%', '%locale_supported%']
tags:
- { name: kernel.event_subscriber, event: kernel.request }

Categories