Symfony2 jms/i18n-routing-bundle and multiple hosts to one locale - symfony

I am using mentioned bundle in my application, and I would like to be able to configure it this way:
jms_i18n_routing:
default_locale: en
locales: [en, de]
strategy: custom
hosts:
en: [mydomain.com, subdomain.domain.com]
de: mydomain.de
redirect_to_host: false
so multiple domains to one locale. I would like to run two similiar websites at one application to have access to the 90% of the code which is similiar and same database. Any tips how could i achieve this? Or maybe there is other bundle/solution more accurate for my problem?

From the configuration you cannot bind multiple domains to one locale.
You can try to extend this class of the bundle:
JMS\I18nRoutingBundle\Router\DefaultLocaleResolver
You need to change this part:
public function resolveLocale(Request $request, array $availableLocales)
{
if ($this->hostMap && isset($this->hostMap[$host = $request->getHost()])) {
return $this->hostMap[$host];
}
...
}
adding a more complex hostMap that supports multiple domains for the same locale.

Related

How can I swtich authorization method by ENV variable?

I meet an unusual problem.
We have a form_login (based on FOS user-bundle). And now we want to change it to hslavich/OneloginSamlBundle for saml auth. But we want to save ability to select an auth method by changing environment vars in kubernetes deployment.
We use k8s on prod with pre-build images (implements "bin/console cache:warmup" in composer scripts for generating cache).
I'm implemented an environment variable for switch needed config.
Than I generate a switcher like this:
return static function (ContainerConfigurator $container) {
$isSamlEnabled = getenv('IS_AUTH_SAML_ENABLE');
if($isSamlEnabled === true) {
$container->import('security_provider_configs/saml.yml');
}
else {
$container->import('security_provider_configs/ldap.yml');
}
};
But this solution use fixed variable IS_AUTH_SAML_ENABLE, which was is in builded image and can't be changed in kubernetes deployment.
We can add new APP_ENV stage, for difference prod-form and prod-saml, we can build two images like 'v2.123-form' and 'v2.123-saml'. But this will brake all CI/CD in our company. It's very difficult.
Do you know any methods to switch auth method by change env variable?
security.yml like this:
security:
providers:
form_usr:
id: my_service.provider.user
saml_provider:
entity:
class: MyService\UserModel
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false
main:
pattern: ^/
saml:
provider: saml_provider
user_factory: user_saml_factory
username_attribute: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
persist_user: true
check_path: /saml/acs
login_path: /saml/login
form_login:
provider: form_usr
default_target_path: about
always_use_default_target_path: true
logout:
target: /login
anonymous: true
Disclaimer: I'm not completely certain that this will work, but this case demands some testing on your part. ;o) Please report back if it indeed does work.
As Nico Haase correctly pointed out, your env vars are resolved at cache/compile-time. However, this is a poster case for APP_ENV (which is usually dev on dev systems and prod on production), you could add/use prod_ldap and prod_saml instead of just prod, which absolutely can have implications on your application. (switching a user from one to the other will not work without some hassle or at least re-login) - see https://symfony.com/doc/4.1/configuration/environments.html for some more information about adding more environments. The documentation is for symfony 4.1 so please don't blindly follow the examples. Stuff has changed, but the general idea should still be viable.
For that to work, you would have to adapt config/bundles.php, and possibly src/Kernel.php and maybe even more things, and you probably have to copy some of the env-specific configs ...
Since all caching and container-building is done depending on APP_ENV and the results are written to a APP_ENV-specific cache location, the containers as well as the caches and sessions would reside in different locations - you'd have the same code base and the same project directory but different cache and config dirs. Unless your application is extremely sophisticated and sensitive to this, this should work.
Please note, that depending on how you're changing your APP_ENV, this might absolutely not work. If it's set by the webserver, I'm confident it should.
Please also note, that to put your system live, you will have to do both bin/console cache:clear --env=prod_saml as well as bin/console cache:clear --env=prod_ldap, composer will run the one in .env(.local) automatically - if you even run composer - but you only can have one at the time. You could extend the composer.json to run both cache:clear commands as a post-something script.
Thanks to #Jakumi for an answer! But this solution have some troubles in our CI/CD.
I solved my problem. It takes to add a private parameter to Kernel:
final class Kernel extends SymfonyKernel
{
...
private $cacheSuffix = '';
and fill it in constructor:
public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
$useSaml = getenv('SAML_ENABLE') === 'true';
$this->cacheSuffix = $useSaml ? '/saml' : '';
}
After that we need to replace Kernel->getCacheDir by custom:
public function getCacheDir(): string
{
return $this->getProjectDir() . '/var/cache/' . $this->getEnvironment() . $this->cacheSuffix;
}
So, we can to warmup cache for both auth methods, for example in composer.json:
...
"scripts": {
"auto-scripts": [
"export SAML_ENABLE=false && bin/console cache:warmup",
"export SAML_ENABLE=true && bin/console cache:warmup"
],
...

Symfony SAAS multitenant (DB by tenant). Sharing my implementation

I people! First: sorry about my english (I'm improving it...). Thanks for edit de post to corret it :)
I'm implementing a SaaS app multitenant on symfony. I wish share with the community what is the way that I've used (mae by not the best, but it works fine...).
THE CONCEPT:
One code (one app).
A DB for each tenant
A CSS for each tenant
Each client have a subdomain like (www.tenant1.myapp.com, www.tenant2.myapp.com)
FIRST: ARRANGING THE CONFIGURATION
I've arranged the configuration files in folders like this:
-app
---config
-----tenantA
--------config.yml
--------config_dev.yml
--------config_prod.yml
--------parameters.yml
-----tenantB
--------config.yml
--------config_dev.yml
--------config_prod.yml
--------parameters.yml
-----services.yml
-----security.yml
DISPATCHER
On AppKernel I've modiffied the registerContainerConfiguration action. It loads de general configuration (DB parameters, twig globals, etc....).
Original:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
New (I used the subdomain to set what is the environment that symfony have to load:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$url= $_SERVER['HTTP_HOST'];
$partes = explode('.',$url);
$tenant= $partes[0]; // this get de subdomain. If the access is www.tenant1.myapp.com this returns tenant1.
$loader->load($this->getRootDir().'/config/'.$tenant.'/config_'.$this->getEnvironment().'.yml');
}
This function goes to the tenant folder and load the config file with his parameters.yml
ARRANGING CACHE AND LOG FOLDERS
I've modified the AppKernel including the functions getCacheDir and getLogDir in order to set the correct folder to save the files:
public function getCacheDir()
{
return $this->rootDir . '/cache/' . $tenant. '/' . $this->environment;
}
public function getLogDir()
{
return $this->rootDir . '/logs/' . $tenant. '/' . $this->environment;
}
This save the files like:
-cache
---tenant1
-----dev
-----prod
---tenant2
-----dev
-----prod
-logs
---tenant1
-----dev.log
-----prod.log
----tenant2
------dev.log
------prod.log
CSS BY TENANT
I've arranged my css files by tenant like this:
--web
---css
----tenant1.css
----tenant2.css
----tenantN.css
Then, in the config.yml of each tenant (remind the arranged folders on app->config) I used a global tiwg with the name of the css theme of the tenant:
twig:
globals:
app_css: tenant1.css
Then, in the tiwg where i've declared the css files and include in the base twig I call the css by:
{% set cssload = "css/" ~ app_css ~ ".css" %}
<link rel="stylesheet" href="/{{cssload}}">
As the cache is saved in diferents folders, the CSS of tenant1 don't affects the cache and the css of tenant2.
Finish: I don't know if this is a good practice, but it works fine, and it's another solution.
Thanks everybody for the help of this days!!!!!

Symfony2 multi-level dynamic router

I have a current project that has to displays both defined pages with specific entities, what is very easy to manage with Symfony2, and content pages on different layouts, what is - I guess - a bit less common.
I get in trouble trying to build the routing system.
For instance, if I have to display a page with some news,
I would like to update the router of my bundle with a new route like :
my_bundle_news_page:
pattern: /news
defaults:
_controller: MyBundle:NewsController:indexAction
But how to manage a dynamic router that could have a totally custom URL on many levels ?
Let's imagine I've got a "Page" Entity, that is self-references for an optionnal "parent-child" relation.
I don't think I can just use any config YAML file for this specific routing ?!
my_bundle_custom_page:
pattern: /{slug}
defaults:
_controller: MyBundle:PageController:showAction
This would bind all the first-level pages:
/projects
/about
/contact
/our-project
What about a page that would be displayed with, for instance, a slug like:
/our-project/health
In fact any URL...
/{slug-level1}/{slug-level2}/{slug-level3} etc.
Cause the pages are supposed to change and be updated from webmastering.
I guess the best way would be to have a router that compare the {slug} with a database field (entity property)
I read in the Symfony-CMF doc that it is possible to write a service based a route provider:
namespace MyBundle\Routing;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route as SymfonyRoute;
use MyBundle\Entity\PageRepository;
class RouteProvider extends PageRepository {
public function findPageBySlug($slug)
{
// Find a page by slug property
$page = $this->findOneBySlug($slug);
if (!$page) {
// Maybe any custom Exception
throw $this->createNotFoundException('The page you are looking for does not exists.');
}
$pattern = $page->getUrl(); // e.g. "/first-level/second-level/third-level"
$collection = new RouteCollection();
// create a new Route and set our page as a default
// (so that we can retrieve it from the request)
$route = new SymfonyRoute($pattern, array(
'page' => $page,
));
// add the route to the RouteCollection using a unique ID as the key.
$collection->add('page_'.uniqid(), $route);
return $collection;
}
}
But how to set it up as a service ? Are there some requirements ?
How could this kind of thing work, does it add a route to the RouteCollection when request is called ?
And will I be able to bind any route in this way ?
EDIT : services.yml of my bundle
parameters:
cmf_routing.matcher.dummy_collection.class: Symfony\Component\Routing\RouteCollection
cmf_routing.matcher.dummy_context.class: Symfony\Component\Routing\RequestContext
cmf_routing.generator.class: Symfony\Cmf\Bundle\RoutingBundle\Routing\ContentAwareGenerator
cmf_routing.nested_matcher.class: Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
cmf_routing.url_matcher.class: Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
fsbcms.chain_router.class: Symfony\Cmf\Component\Routing\ChainRouter
fsbcms.route_provider.class: FSB\CMSBundle\Routing\RouteProvider
fsbcms.dynamic_router.class: Symfony\Cmf\Component\Routing\DynamicRouter
fsbcms.route_entity.class: null
services:
fsbcms.router:
class: %fsbcms.chain_router.class%
arguments:
- "#logger"
calls:
- [setContext, ["router.request_context"]]
fsbcms.route_provider:
class: "%fsbcms.route_provider.class%"
arguments:
- "#doctrine"
cmf_routing.matcher.dummy_collection:
class: "%cmf_routing.matcher.dummy_collection.class%"
public: "false"
cmf_routing.matcher.dummy_context:
class: "%cmf_routing.matcher.dummy_context.class%"
public: false
cmf_routing.generator:
class: "%cmf_routing.generator.class%"
arguments:
- "#fsbcms.route_provider"
- "#logger"
calls:
- [setContainer, ["service_container"]]
- [setContentRepository, ["cmf_routing.content_repository"]]
cmf_routing.url_matcher:
class: "%cmf_routing.url_matcher.class%"
arguments: ["#cmf_routing.matcher.dummy_collection", "#cmf_routing.matcher.dummy_context"]
cmf_routing.nested_matcher:
class: "%cmf_routing.nested_matcher.class%"
arguments: ["#fsbcms.route_provider"]
calls:
- [setFinalMatcher, ["cmf_routing.url_matcher"]]
fsbcms.dynamic_router:
class: "%fsbcms.dynamic_router.class%"
arguments:
- "#router.request_context"
- "#cmf_routing.nested_matcher"
- "#cmf_routing.generator"
tags:
- { name: router, priority: 300 }
I suggest taking a look at the Symfony CMF routing component and the CmfRoutingBundle (to implement the component in symfony).
The Routing component uses a chain router, which is irrelevant for this question but it's good to know. The chain router chains over a queue of routers. The component provides a DynamicRouter that uses a NestedMatcher. That's exactly what you want.
The NestedMatcher uses a Route provider to get the routes from a dynamic source (e.g. a database). You are showing an example of a Route provider in your question.
Furthermore, it uses a FinalMatcher to match the route. You can just pass an instance of Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher, as you are doing not too difficult things.
Take a look at the docs of the RoutingBundle to learn how to activate the chain router and then create a route provider which loads the routes, make a service:
acme_routing.route_provider:
class: Acme\RoutingBundle\Provider\DoctrineOrmProvider
arguments: ["#doctrine"]
Now, you can create a NestedMatcher service:
acme_routing.url_matcher:
class: Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher
arguments: ["#cmf_routing.matcher.dummy_collection", "#cmf_routing.matcher.dummy_context"]
acme_routing.nested_matcher:
class: Symfony\Cmf\Component\Routing\NestedMatcher
arguments: ["#acme_routing.route_provider"]
calls:
- [setFinalMatcher, ["acme_routing.url_matcher"]]
Now, register the DynamicRouter and put it in the chain:
acme_routing.dynamic_router:
class: Symfony\Cmf\Component\Routing\DynamicRouter
arguments:
- "#router.request_context"
- "#acme_routing.nested_matcher"
- "#cmf_routing.generator"
tags:
- { name: router, priority: 300 }
Now, it should work and should load the routes from the database and match them against the request.

Symfony2.2 : default_locale always applying in twig translations

I'm having a strange issue with Symfony2.2. I have a project using two languages : en/fr. So I create as usual (like Symfony2.0) two translation files "messages.en.yml" and "messages.fr.yml" in Ressources/Views/translations/. But Translations in twig could not change even if we set the request object and the locale session. Translation is always set by the default_locale (config.php).
Example : if default_locale = en, all my website (in twig) is translated in en, even if i set the _locale object in fr (request and session). Of course if I manually change the default_locale to fr the website is naturally in fr...
However, _locale session works but I don't know if locale request works, and of course translation works in controllers too...
There is my files :
config.yml:
framework:
#esi: ~
translator: { fallback: %locale% } # = en
# ...
default_locale: %locale% # = en
Controller :
public function indexAction()
{
$this->get('session')->set('_locale', 'fr');
$this->getRequest()->setLocale($lang);
exit($this->getRequest()->getLocale()); // = fr
exit($this->get('translator')->trans('Symfony2 is great')); // = Symfony2 est génial
return $this->render('TestBundle:Controller:test.html.twig');
View :
{% block content %}
<p>lang : {{ app.request.locale }}</p> {#} = "fr", OK{#}
<p>{{ 'Symfony2 is great'|trans }}</p> {#} = "Symfony2 is great", WAIT WHAT?{#}
I must resign myself to force the locale at the beginning of the method controller to have the requested locale (stored in session) like that :
Controller:
if($this->get('session')->get('_locale')){
$lang = $this->get('session')->get('_locale');
$this->getRequest()->setLocale($lang);
}
In other words, I do have a problem with the registration of the request object... Because the last code works well in the controller, and shows well the locale in twig page with app.request.locale, but not the translations... (sorry for my bad english and thanks for helping)
I had the same issue due to the low priority of my event listener. The locale would be overridden by the Translator's TranslatorListener. Increasing the priority of my event listener did the trick for me:
services:
app.locale_listener:
class: AppBundle\EventListener\LocaleListener
tags:
- { name: kernel.event_listener, priority: 11, ... }
Source: https://github.com/symfony/symfony/issues/12878#issuecomment-68628808
Parameter _locale in routing holds your locale value.
Look here on this page
Symfony - Book - Translation - Local and the URL
From Symfony 2.1 they have this kind of logic:
Since you can store the locale of the user in the session, it may be tempting to use the same URL to display a resource in many different languages based on the user's locale. For example, http://www.example.com/contact could show content in English for one user and French for another user. Unfortunately, this violates a fundamental rule of the Web: that a particular URL returns the same resource regardless of the user. To further muddy the problem, which version of the content would be indexed by search engines?
A better policy is to include the locale in the URL. This is fully-supported by the routing system using the special _locale parameter:
Now when you want to sel local, this doesn't work any more
$this->get('session')->set('_locale', 'fr');
You can use request insted of session now but you can not have session-logic with _local from Symfony 2.0 unless you simulate it with event listener on kernel request.

Custom route configuration with Silex

I know that the basis of Silex approach in which all the application logic in a single file. But my application will be possible to have more than twenty controllers. So I want to have a handy map to manage the router.
My question is to search for solutions in which I would be able to make a router to a separate file. In the best case, the file must be of YAML type:
# config/routing.yml
_home:
pattern: /
defaults: { _controller: MyProject\Controller\MyController::index }
But the native is also a good case (for me):
$routes = new RouteCollection();
$routes->add(
'home',
new Route('/', array('controller' => 'MyProject\Controller\MyController::index')
));
return $routes;
Problem of the second case is that I have to use the match() function for each rule of routing. It is not at all clear.
What are the ways to solve this issue? The condition is that I want to use the existing API Silex or components of Symfony2.
Small note:
I don't use a ControllerProviderInterface for my Controller classes. This is an independent classes.
First of all, the basis of Silex is not that you put everything in one file. The basis of Silex is that you create your own 'framework', your own way of organizing applications.
"Use silex if you are comfortable with making all of your own architecture decisions and full stack Symfony2 if not."
-- Dustin Whittle
Read more about this in this blogpost, created by the creator of Silex.
How to solve your problem
What you basically want is to parse a Yaml file and get the pattern and defaults._controller settings from each route that is parsed.
To parse a Yaml file, you can use the Yaml Component of Symfony2. You get an array back which you can use to add the route to Silex:
// parse the yaml file
$routes = ...;
$app = new Silex\Application();
foreach ($routes as $route) {
$app->match($route['pattern'], $route['defaults']['_controller']);
}
// ...
$app->run();
I thought I'd add my method here as, although others may work, there isn't really a simple solution. Adding FileLocator / YamlFileLoader adds a load of bulk that I don't want in my application just to read / parse a yaml file.
Composer
First, you're going to need to include the relevant files. The symfony YAML component, and a really simple and useful config service provider by someone who actively works on Silex.
"require": {
"symfony/yaml": "~2.3",
"igorw/config-service-provider": "1.2.*"
}
File
Let's say that your routes file looks like this (routes.yml):
config.routes:
dashboard:
pattern: /
defaults: { _controller: 'IndexController::indexAction' }
method: GET
Registration
Individually register each yaml file. The first key in the file is the name it will be available under your $app variable (handled by the pimple service locator).
$this->register(new ConfigServiceProvider(__DIR__."/../config/services.yml"));
$this->register(new ConfigServiceProvider(__DIR__."/../config/routes.yml"));
// any more yaml files you like
Routes
You can get these routes using the following:
$routes = $app['config.routes']; // See the first key in the yaml file for this name
foreach ($routes as $name => $route)
{
$app->match($route['pattern'], $route['defaults']['_controller'])->bind($name)->method(isset($route['method'])?$route['method']:'GET');
}
->bind() allows you to 'name' your urls to be used within twig, for example.
->method() allows you to specify POST | GET. You'll note that I defaulted it to 'GET' with a ternary there if the route doesn't specify a method.
Ok, that's how I solved it.
This method is part of my application and called before run():
# /src/Application.php
...
protected function _initRoutes()
{
$locator = new FileLocator(__DIR__.'/config');
$loader = new YamlFileLoader($locator);
$this['routes'] = $loader->load('routes.yml');
}
Application class is my own and it extends Silex\Application.
Configuration file:
# /src/config/routes.yml
home:
pattern: /
defaults: { _controller: '\MyDemoSite\Controllers\DefaultController::indexAction' }
It works fine for me!
UPD:
I think this is the right option to add collections:
$this['routes']->addCollection($loader->load('routes.yml'));
More flexible.
You could extend the routes service (which is a RouteCollection), and load a YAML file with FileLocator and YamlFileLoader:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
$app->extend('routes', function($routeCollection) {
$locator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
$routeCollection->addCollection($collection);
return $routeCollection;
});
You will need symfony/config and symfony/yaml dependencies though.

Resources