├── .gitignore
├── slides.pdf
├── Tests
├── Functional
│ ├── DemoApp
│ │ ├── DemoBundle
│ │ │ ├── Resources
│ │ │ │ └── views
│ │ │ │ │ └── Test
│ │ │ │ │ ├── _foo_
│ │ │ │ │ └── header.html.twig
│ │ │ │ │ ├── header.html.twig
│ │ │ │ │ ├── __en_GB
│ │ │ │ │ └── header.html.twig
│ │ │ │ │ ├── _bar_de_DE
│ │ │ │ │ └── header.html.twig
│ │ │ │ │ ├── locale.html.twig
│ │ │ │ │ └── index.html.twig
│ │ │ ├── AlexMultisiteDemoBundle.php
│ │ │ └── Controller
│ │ │ │ └── TestController.php
│ │ ├── routing.yml
│ │ ├── AppKernel.php
│ │ └── config.yml
│ ├── DemoApp_NoSort
│ │ ├── routing.yml
│ │ ├── DemoBundle
│ │ │ ├── AlexMultisiteDemoBundle.php
│ │ │ └── Controller
│ │ │ │ └── TestController.php
│ │ ├── config.yml
│ │ └── AppKernel.php
│ ├── DemoApp_Security
│ │ ├── routing.yml
│ │ ├── DemoBundle
│ │ │ ├── Resources
│ │ │ │ └── views
│ │ │ │ │ └── Test
│ │ │ │ │ ├── homepage.html.twig
│ │ │ │ │ └── login.html.twig
│ │ │ ├── AlexMultisiteDemoBundle.php
│ │ │ └── Controller
│ │ │ │ └── TestController.php
│ │ ├── AppKernel.php
│ │ └── config.yml
│ ├── WebTestCase.php
│ ├── TemplatingTest.php
│ ├── SecurityTest.php
│ ├── ConfigTest.php
│ ├── RoutingTest.php
│ └── AbstractAppKernel.php
└── Branding
│ ├── BrandingTest.php
│ └── SiteContextTest.php
├── autoload.php
├── CONTRIBUTORS.md
├── composer.json
├── .travis.yml
├── Resources
└── config
│ ├── framework_extra.xml
│ ├── site_context.xml
│ └── twig.xml
├── CHANGELOG.md
├── AlexMultisiteBundle.php
├── DependencyInjection
├── Compiler
│ ├── TwigLoaderPass.php
│ └── InjectSiteContextPass.php
├── Configuration.php
└── AlexMultisiteExtension.php
├── Twig
├── MultisiteExtension.php
└── MultisiteLoader.php
├── phpunit.xml.dist
├── LICENSE
├── Annotation
└── Route.php
├── Branding
├── Branding.php
└── SiteContext.php
├── Router
├── Loader
│ └── AnnotatedRouteControllerLoader.php
└── MultisiteRouter.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /composer.lock
3 |
--------------------------------------------------------------------------------
/slides.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexandresalome/multisite-bundle/HEAD/slides.pdf
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Resources/views/Test/_foo_/header.html.twig:
--------------------------------------------------------------------------------
1 | Foo header
2 | ==========
3 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Resources/views/Test/header.html.twig:
--------------------------------------------------------------------------------
1 | Default header
2 | ==============
3 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Resources/views/Test/__en_GB/header.html.twig:
--------------------------------------------------------------------------------
1 | English header
2 | ==============
3 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/routing.yml:
--------------------------------------------------------------------------------
1 | _demo:
2 | resource: "@AlexMultisiteDemoBundle/Controller"
3 | type: annotation
4 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Resources/views/Test/_bar_de_DE/header.html.twig:
--------------------------------------------------------------------------------
1 | German header on bar
2 | ====================
3 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_NoSort/routing.yml:
--------------------------------------------------------------------------------
1 | _demo:
2 | resource: "@AlexMultisiteDemoBundle/Controller"
3 | type: annotation
4 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_Security/routing.yml:
--------------------------------------------------------------------------------
1 | _demo:
2 | resource: "@AlexMultisiteDemoBundle/Controller"
3 | type: annotation
4 |
--------------------------------------------------------------------------------
/autoload.php:
--------------------------------------------------------------------------------
1 | Homepage {{ site_context.currentBranding.name }}/{{ site_context.currentLocale }}
2 |
3 |
Login
4 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/AlexMultisiteDemoBundle.php:
--------------------------------------------------------------------------------
1 | Login form
2 |
3 |
8 |
9 | Go back to homepage
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 | - 7.0
9 | - hhvm
10 |
11 | env:
12 | - SYMFONY_VERSION="2.4.*"
13 | - SYMFONY_VERSION="2.8.*"
14 | - SYMFONY_VERSION="3.0.*"
15 |
16 | matrix:
17 | exclude:
18 | - php: 5.3
19 | env: SYMFONY_VERSION="3.0.*"
20 | - php: 5.4
21 | env: SYMFONY_VERSION="3.0.*"
22 |
23 | before_script:
24 | - composer require symfony/symfony:${SYMFONY_VERSION}
25 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Resources/views/Test/index.html.twig:
--------------------------------------------------------------------------------
1 | {{ include('AlexMultisiteDemoBundle:Test:header.html.twig') }}
2 | Test homepage
3 |
4 | The index page
5 | Default link to locale page
6 | Link with locale
7 | Link with branding
8 | Link with branding
9 |
--------------------------------------------------------------------------------
/Resources/config/framework_extra.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Alex\MultisiteBundle\Router\Loader\AnnotatedRouteControllerLoader
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Tests/Functional/WebTestCase.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new InjectSiteContextPass());
15 | $container->addCompilerPass(new TwigLoaderPass());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_NoSort/AppKernel.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 | %alex_multisite.default_branding%
12 | %alex_multisite.default_locale%
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/TwigLoaderPass.php:
--------------------------------------------------------------------------------
1 | getAlias('twig.loader');
19 | $container->setAlias('twig.loader', 'twig.loader.multisite');
20 | $container->setAlias('twig.loader.previous', $alias);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Twig/MultisiteExtension.php:
--------------------------------------------------------------------------------
1 | siteContext = $siteContext;
14 | }
15 |
16 | /**
17 | * {@inheritdoc}
18 | */
19 | public function getGlobals()
20 | {
21 | return array(
22 | 'site_context' => $this->siteContext
23 | );
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getName()
30 | {
31 | return 'multisite';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Resources/config/twig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Tests/Functional/TemplatingTest.php:
--------------------------------------------------------------------------------
1 | request('GET', 'http://foo.example.org/page-en');
15 | $this->assertContains('Foo header', $client->getResponse()->getContent());
16 |
17 | // bar - fr_FR
18 | $client->request('GET', 'http://bar.example.org/page-fr');
19 | $this->assertContains('Default header', $client->getResponse()->getContent());
20 |
21 | // bar - de_DE
22 | $client->request('GET', 'http://de.bar.example.org/page-de');
23 | $this->assertContains('German header', $client->getResponse()->getContent());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/Functional/SecurityTest.php:
--------------------------------------------------------------------------------
1 | request('GET', '/bar-en/');
14 | $this->assertContains('Homepage bar/en_GB', $client->getResponse()->getContent());
15 |
16 | $crawler = $client->click($crawler->filter('a:contains("Login")')->eq(0)->link());
17 | $this->assertContains('Login form', $client->getResponse()->getContent());
18 |
19 | $form = $crawler->selectButton('Submit')->form(array(
20 | '_username' => 'user',
21 | '_password' => 'user'
22 | ));
23 |
24 | $client->submit($form);
25 |
26 | $crawler = $client->followRedirect();
27 | // make sure we stayed on same site
28 | $this->assertContains('Homepage bar/en_GB', $client->getResponse()->getContent());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_NoSort/DemoBundle/Controller/TestController.php:
--------------------------------------------------------------------------------
1 | request('GET', 'http://foo.example.org/page-en');
15 | $this->assertContains('flag A', $client->getResponse()->getContent());
16 |
17 | // foo - fr_FR
18 | $client->request('GET', 'http://foo.example.org/fr/page-fr');
19 | $this->assertNotContains('flag A', $client->getResponse()->getContent());
20 |
21 | // bar - fr_FR
22 | $client->request('GET', 'http://bar.example.org/page-fr');
23 | $this->assertNotContains('flag B', $client->getResponse()->getContent());
24 | }
25 |
26 | public function testNoSort()
27 | {
28 | $client = self::createClient('DemoApp_NoSort');
29 |
30 | $client->request('GET', '/bar');
31 | $this->assertContains('Route A', $client->getResponse()->getContent());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/InjectSiteContextPass.php:
--------------------------------------------------------------------------------
1 | getDefinition('sensio_framework_extra.routing.loader.annot_class')->addMethodCall('setSiteContext', array(new Reference('site_context')));
20 | $container->getDefinition('router.default')->setClass('Alex\MultisiteBundle\Router\MultisiteRouter');
21 | $container->getDefinition('router.default')->addMethodCall('setSiteContext', array(new Reference('site_context')));
22 | $container->getDefinition('router.default')->addMethodCall('setSortRoutes', array("%alex_multisite.sort_routes%"));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 | Tests
19 |
20 |
21 |
22 |
23 |
24 | .
25 |
26 | Resources
27 | Tests
28 | vendor
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Alexandre Salomé
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/Annotation/Route.php:
--------------------------------------------------------------------------------
1 | paths = $paths;
44 |
45 | return $this;
46 | }
47 |
48 | /**
49 | * @return array|null
50 | */
51 | public function getPaths()
52 | {
53 | return $this->paths;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_Security/config.yml:
--------------------------------------------------------------------------------
1 | framework:
2 | secret: foo
3 | router:
4 | resource: "%kernel.root_dir%/routing.yml"
5 | templating: { engine: twig }
6 | test: ~
7 | session:
8 | storage_id: session.storage.mock_file
9 |
10 | security:
11 | providers:
12 | in_memory:
13 | memory:
14 | users:
15 | user:
16 | password: user
17 | roles: 'ROLE_USER'
18 | encoders:
19 | Symfony\Component\Security\Core\User\User: plaintext
20 | firewalls:
21 | default:
22 | anonymous: ~
23 | form_login:
24 | login_path: login
25 | check_path: login-check
26 | default_target_path: homepage
27 | logout:
28 | path: logout
29 |
30 | alex_multisite:
31 | default_branding: foo
32 | default_locale: fr_FR
33 | brandings:
34 | foo:
35 | en_GB: { prefix: /foo-en }
36 | fr_FR: { prefix: /foo-fr }
37 | bar:
38 | fr_FR: { prefix: /bar-fr }
39 | en_GB: { prefix: /bar-en }
40 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | root('alex_multisite');
20 |
21 | $rootNode
22 | ->children()
23 | ->booleanNode('sort_routes')->defaultTrue()->end()
24 | ->scalarNode('default_branding')->isRequired()->end()
25 | ->scalarNode('default_locale')->isRequired()->end()
26 | ->arrayNode('default_config')
27 | ->prototype('scalar')->end()
28 | ->end()
29 | ->arrayNode('brandings')
30 | ->prototype('variable')
31 | ->end()
32 | ->end()
33 | ->end()
34 | ;
35 |
36 | return $treeBuilder;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp/DemoBundle/Controller/TestController.php:
--------------------------------------------------------------------------------
1 | request('GET', $url);
23 | $this->assertContains('Test homepage', $client->getResponse()->getContent(), "Test ".$url);
24 | }
25 | }
26 |
27 | public function testLocaleOnly()
28 | {
29 | $client = self::createClient();
30 |
31 | // foo - en_GB
32 | $client->request('GET', 'http://foo.example.org/page-en');
33 | $this->assertContains('branding: foo, locale: en_GB', $client->getResponse()->getContent());
34 |
35 | // foo - fr_FR
36 | $client->request('GET', 'http://foo.example.org/fr/page-fr');
37 | $this->assertContains('branding: foo, locale: fr_FR', $client->getResponse()->getContent());
38 |
39 | // bar - fr_FR
40 | $client->request('GET', 'http://bar.example.org/page-fr');
41 | $this->assertContains('branding: bar, locale: fr_FR', $client->getResponse()->getContent());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/Branding/BrandingTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('foo', $branding->getName());
13 | }
14 |
15 | public function testHasLocale()
16 | {
17 | $branding = new Branding('foo', array(
18 | 'fr_FR' => array('host' => 'foo')
19 | ));
20 |
21 | $this->assertFalse($branding->hasLocale('en_GB'));
22 | $this->assertTrue($branding->hasLocale('fr_FR'));
23 | }
24 |
25 | public function testGetHost()
26 | {
27 | $branding = new Branding('foo', array(
28 | 'fr_FR' => array('host' => 'foo')
29 | ));
30 |
31 | $this->assertEquals('foo', $branding->getHost('fr_FR'));
32 | $this->assertNull($branding->getHost('en_GB'));
33 | }
34 |
35 | public function testGetOption()
36 | {
37 | $branding = new Branding('foo', array('fr_FR' => array('bar' => 'bar')));
38 | $this->assertEquals('bar', $branding->getOption('fr_FR', 'bar'));
39 | }
40 |
41 | public function testPrefixPath()
42 | {
43 | $branding = new Branding('foo', array(
44 | 'fr_FR' => array('prefix' => '/test')
45 | ));
46 |
47 | $this->assertEquals('/test/fr', $branding->prefixPath('fr_FR', '/fr'));
48 | $this->assertEquals('/fr', $branding->prefixPath('en_GB', '/fr'));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/Functional/DemoApp_Security/DemoBundle/Controller/TestController.php:
--------------------------------------------------------------------------------
1 | getSession();
34 | $error = null;
35 |
36 | if ($request->attributes->has('_security.last_error')) {
37 | $error = $request->attributes->get('_security.last_error');
38 | } elseif ($session !== null && $session->has('_security.last_error')) {
39 | $error = $session->get('_security.last_error');
40 | $session->remove('_security.last_error');
41 | }
42 |
43 | return array('error' => $error);
44 | }
45 |
46 |
47 | /**
48 | * @Route(name="login-check", path="/login-check")
49 | */
50 | public function loginCheckAction()
51 | {
52 | throw new \LogicException('Should not be executed.');
53 | }
54 |
55 | /**
56 | * @Route(name="logout", paths={
57 | * "fr_FR"="/deconnexion",
58 | * "en_GB"="/logout",
59 | * })
60 | */
61 | public function logoutAction()
62 | {
63 | throw new \LogicException('Should not be executed.');
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/Functional/AbstractAppKernel.php:
--------------------------------------------------------------------------------
1 | name) {
16 | return $this->name;
17 | }
18 |
19 | $class = get_class($this);
20 | $posEnd = strrpos($class, '\\');
21 | $posStart = strrpos($class, '\\', $posEnd - strlen($class) - 1) + 1;
22 |
23 | $this->name = strtolower(substr($class, $posStart, $posEnd - $posStart));
24 |
25 | return $this->name;
26 | }
27 |
28 | public function getDirectory()
29 | {
30 | if ($this->directory) {
31 | return $this->directory;
32 | }
33 |
34 | $this->directory = sys_get_temp_dir().'/test_ms_'.$this->getName();
35 |
36 | if (!is_dir($this->directory)) {
37 | mkdir($this->directory, 0777, true);
38 | }
39 |
40 | return $this->directory;
41 | }
42 |
43 | public function getAdditionalBundles()
44 | {
45 | return array();
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function registerBundles()
52 | {
53 | $bundles = array(
54 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
55 | new \Symfony\Bundle\TwigBundle\TwigBundle(),
56 | new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
57 | new \Alex\MultisiteBundle\AlexMultisiteBundle(),
58 | );
59 |
60 | return array_merge($bundles, $this->getAdditionalBundles());
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function registerContainerConfiguration(LoaderInterface $loader)
67 | {
68 | $refl = new \ReflectionClass($this);
69 | $dir = dirname($refl->getFilename());
70 |
71 | $loader->load($dir.'/config.yml');
72 | }
73 |
74 | public function getCacheDir()
75 | {
76 | return $this->getDirectory();
77 | }
78 |
79 | public function getLogDir()
80 | {
81 | return $this->getDirectory();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/Branding/SiteContextTest.php:
--------------------------------------------------------------------------------
1 | getSiteContext();
13 | $this->assertNull($context->getOption('foo'));
14 | $this->assertEquals('default', $context->getOption('inexisting', 'default'));
15 | $context->setCurrentLocale('en_GB');
16 | $this->assertTrue($context->getOption('foo'));
17 | }
18 |
19 | public function testNormalizePaths_WithDetails()
20 | {
21 | $context = $this->getSiteContext();
22 |
23 | $actual = array(
24 | 'foo' => array('fr_FR' => '/french'),
25 | 'bar' => array('de_DE' => '/german')
26 | );
27 |
28 | $expected = array(
29 | 'foo' => array(
30 | 'fr_FR' => '/french'
31 | ),
32 | 'bar' => array(
33 | 'de_DE' => '/de/german'
34 | ),
35 | );
36 |
37 | $this->assertEquals($expected, $context->normalizePaths($actual));
38 | }
39 |
40 | public function testNormalizePaths_WithLocale()
41 | {
42 | $context = $this->getSiteContext();
43 |
44 | $actual = array(
45 | 'fr_FR' => '/french',
46 | 'en_GB' => '/english',
47 | 'de_DE' => '/german',
48 | );
49 |
50 | $expected = array(
51 | 'foo' => array(
52 | 'fr_FR' => '/french',
53 | 'en_GB' => '/english',
54 | ),
55 | 'bar' => array(
56 | 'fr_FR' => '/french',
57 | 'en_GB' => '/en/english',
58 | 'de_DE' => '/de/german',
59 | ),
60 | );
61 |
62 | $this->assertEquals($expected, $context->normalizePaths($actual));
63 | }
64 |
65 | private function getSiteContext()
66 | {
67 | $foo = new Branding('foo', array(
68 | 'fr_FR' => array('host' => 'foo.fr'),
69 | 'en_GB' => array('foo' => true, 'host' => 'foo.co.uk'),
70 | ));
71 |
72 | $bar = new Branding('bar', array(
73 | 'fr_FR' => array('host' => 'bar.com'),
74 | 'en_GB' => array('host' => 'bar.com', 'prefix' => '/en'),
75 | 'de_DE' => array('host' => 'bar.com', 'prefix' => '/de')
76 | ));
77 |
78 | return new SiteContext(array($foo, $bar), 'foo', 'fr_FR');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/DependencyInjection/AlexMultisiteExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration(new Configuration(), $configs);
23 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
24 |
25 | $container->setParameter('alex_multisite.default_branding', $config['default_branding']);
26 | $container->setParameter('alex_multisite.default_locale', $config['default_locale']);
27 | $container->setParameter('alex_multisite.sort_routes', $config['sort_routes']);
28 |
29 | $loader->load('site_context.xml');
30 | $loader->load('framework_extra.xml');
31 | $loader->load('twig.xml');
32 |
33 | $this->addBrandingDefinition($container, $config['brandings']);
34 | }
35 |
36 | /**
37 | * Adds configured brandings to the site context service.
38 | *
39 | * @param ContainerBuilder $container
40 | * @param array $options
41 | */
42 | private function addBrandingDefinition(ContainerBuilder $container, array $options)
43 | {
44 | $brandings = array();
45 |
46 | if (isset($options['_defaults'])) {
47 | $globalOptions = $options['_defaults'];
48 | unset($options['_defaults']);
49 | } else {
50 | $globalOptions = array();
51 | }
52 |
53 | foreach ($options as $name => $localeOptions) {
54 | if (isset($localeOptions['_defaults'])) {
55 | $brandingOptions = $localeOptions['_defaults'];
56 | unset($localeOptions['_defaults']);
57 | } else {
58 | $brandingOptions = array();
59 | }
60 |
61 | $arg = array();
62 | foreach ($localeOptions as $locale => $options) {
63 | $arg[$locale] = array_merge($globalOptions, $brandingOptions, $options);
64 | }
65 |
66 | $brandings[] = new Definition(
67 | 'Alex\MultisiteBundle\Branding\Branding',
68 | array($name, $arg)
69 | );
70 | }
71 |
72 | $container->getDefinition('site_context')->replaceArgument(0, $brandings);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Branding/Branding.php:
--------------------------------------------------------------------------------
1 | array('host' => 'example.org', 'prefix' => '/fr'),
12 | * 'en_GB' => array('host' => 'example.org'),
13 | * );
14 | */
15 | class Branding
16 | {
17 | /**
18 | * @var string
19 | */
20 | private $name;
21 |
22 | /**
23 | * @var array
24 | */
25 | private $localesConfig;
26 |
27 | /**
28 | * Constructor of a branding.
29 | *
30 | * @param string $name
31 | * @param array $localesConfig
32 | */
33 | public function __construct($name, array $localesConfig)
34 | {
35 | $this->name = $name;
36 | $this->localesConfig = $localesConfig;
37 | }
38 |
39 | /**
40 | * Returns the name of the branding.
41 | *
42 | * @return string
43 | */
44 | public function getName()
45 | {
46 | return $this->name;
47 | }
48 |
49 | /**
50 | * Tests if branding has a given locale.
51 | *
52 | * @return boolean
53 | */
54 | public function hasLocale($locale)
55 | {
56 | return isset($this->localesConfig[$locale]);
57 | }
58 |
59 | /**
60 | * Returns host configured for a locale, or null if not found.
61 | *
62 | * @param string $locale
63 | *
64 | * @return string|null returns a hostname or null if not found.
65 | */
66 | public function getHost($locale)
67 | {
68 | if (!isset($this->localesConfig[$locale]['host'])) {
69 | return null;
70 | }
71 |
72 | return $this->localesConfig[$locale]['host'];
73 | }
74 |
75 | /**
76 | * Prefixes the path for a given locale, if a prefix is configured.
77 | *
78 | * ``prefixPath`` will return the path if no prefix is configured.
79 | *
80 | * @param string $locale
81 | * @param string $path
82 | *
83 | * @return string
84 | */
85 | public function prefixPath($locale, $path)
86 | {
87 | if (isset($this->localesConfig[$locale]['prefix'])) {
88 | return $this->localesConfig[$locale]['prefix'].$path;
89 | }
90 |
91 | return $path;
92 | }
93 |
94 | /**
95 | * Returns value of an option.
96 | *
97 | * @param string $locale
98 | * @param string $name
99 | * @param mixed $default
100 | *
101 | * @return mixed
102 | */
103 | public function getOption($locale, $name, $default = null)
104 | {
105 | return isset($this->localesConfig[$locale][$name]) ? $this->localesConfig[$locale][$name] : $default;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Twig/MultisiteLoader.php:
--------------------------------------------------------------------------------
1 | loader = $loader;
32 | $this->siteContext = $siteContext;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function getSource($name)
39 | {
40 | $templates = $this->getTemplates($name);
41 |
42 | foreach ($templates as $template) {
43 | try {
44 | return $this->loader->getSource($template);
45 | } catch (\Twig_Error $e) {
46 | }
47 | }
48 |
49 | throw new \Twig_Error_Loader(sprintf("Template \"%s\" not found. Tried the following:\n%s", $name, implode("\n", $templates)));
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | protected function findTemplate($name)
56 | {
57 | return $this->loader->findTemplate($name);
58 | }
59 |
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function getCacheKey($name)
65 | {
66 | $templates = $this->getTemplates($name);
67 |
68 | foreach ($templates as $template) {
69 | try {
70 | return $this->loader->getCacheKey($template);
71 | } catch (\Twig_Error $e) {
72 | }
73 | }
74 |
75 | throw new \Twig_Error_Loader(sprintf("Template \"%s\" not found. Tried the following:\n%s", $name, implode("\n", $templates)));
76 | }
77 |
78 | /**
79 | * {@inheritdoc}
80 | */
81 | public function isFresh($name, $time)
82 | {
83 | $templates = $this->getTemplates($name);
84 |
85 | foreach ($templates as $template) {
86 | try {
87 | return $this->loader->isFresh($template, $time);
88 | } catch (\Twig_Error $e) {
89 | }
90 | }
91 |
92 | throw new \Twig_Error_Loader(sprintf("Template \"%s\" not found. Tried the following:\n%s", $name, implode("\n", $templates)));
93 | }
94 |
95 | /**
96 | * {@inheritdoc}
97 | */
98 | private function getTemplates($name)
99 | {
100 | $posA = strrpos($name, ':');
101 | $posB = strrpos($name, '/');
102 | $posC = strrpos($name, '/');
103 |
104 | $b = $this->siteContext->getCurrentBrandingName();
105 | $l = $this->siteContext->getCurrentLocale();
106 |
107 | if ($posA === false && $posB === false && $posC === false) {
108 | $prefix = '';
109 | $suffix = '/'.$name;
110 | } else {
111 | $pos = max($posA, $posB, $posC);
112 | $prefix = substr($name, 0, $pos + 1);
113 | $suffix = '/'.substr($name, $pos + 1);
114 | }
115 |
116 | return array(
117 | $prefix.'_'.$b.'_'.$l.''.$suffix,
118 | $prefix.'_'.$b.'_'.$suffix,
119 | $prefix.'__'.$l.''.$suffix,
120 | $name
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Router/Loader/AnnotatedRouteControllerLoader.php:
--------------------------------------------------------------------------------
1 | siteContext = $siteContext;
31 |
32 | return $this;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
39 | {
40 | // If the annotation is not the Multisite's one, call parent method
41 | if (!$annot instanceof RouteAnnotation) {
42 | return parent::addRoute($collection, $annot, $globals, $class, $method);
43 | }
44 |
45 | // mono-route
46 | if (null === $annot->getPaths()) {
47 | return parent::addRoute($collection, $annot, $globals, $class, $method);
48 | }
49 |
50 |
51 | return $this->addMultisiteRoute($collection, $annot, $globals, $class, $method);
52 | }
53 |
54 | /**
55 | * Specific method to add a multisite route to the collection.
56 | *
57 | * Paths option should not be null.
58 | */
59 | protected function addMultisiteRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
60 | {
61 | $paths = $this->siteContext->normalizePaths($annot->getPaths());
62 |
63 | foreach ($paths as $branding => $locales) {
64 | foreach ($locales as $locale => $path) {
65 |
66 | // this block of code is copied from Symfony\Component\Routing\Loader\AnnotationFileLoader
67 | $name = $annot->getName();
68 | if (null === $name) {
69 | $name = $this->getDefaultRouteName($class, $method);
70 | }
71 | $name = MultisiteRouter::ROUTE_PREFIX.'_'.$branding.'_'.$locale.'__'.$name;
72 |
73 | $defaults = array_replace($globals['defaults'], $annot->getDefaults());
74 | foreach ($method->getParameters() as $param) {
75 | if (!isset($defaults[$param->getName()]) && $param->isOptional()) {
76 | $defaults[$param->getName()] = $param->getDefaultValue();
77 | }
78 | }
79 |
80 | // +2 lines
81 | $defaults['_branding'] = $branding;
82 | $defaults['_locale'] = $locale;
83 |
84 | $requirements = array_replace($globals['requirements'], $annot->getRequirements());
85 | $options = array_replace($globals['options'], $annot->getOptions());
86 | $schemes = array_replace($globals['schemes'], $annot->getSchemes());
87 | $methods = array_replace($globals['methods'], $annot->getMethods());
88 |
89 | $host = $annot->getHost();
90 | if (null === $host) {
91 | $host = $globals['host'];
92 | }
93 |
94 | // +3 lines
95 | if (!$host) {
96 | $host = $this->siteContext->getBranding($branding)->getHost($locale);
97 | }
98 |
99 | $condition = $annot->getCondition();
100 | if (null === $condition) {
101 | $condition = $globals['condition'];
102 | }
103 |
104 | $route = new Route($globals['path'].$path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
105 | $this->configureRoute($route, $class, $method, $annot);
106 |
107 | $collection->add($name, $route);
108 | }
109 | }
110 |
111 | // cache will refresh when file is modified
112 | $collection->addResource(new FileResource($class->getFileName()));
113 |
114 | return $collection;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Router/MultisiteRouter.php:
--------------------------------------------------------------------------------
1 | sortRoutes = $value;
35 | }
36 |
37 | /**
38 | * Changes the site context.
39 | *
40 | * @param SiteContext $siteContext
41 | *
42 | * @return MultisiteRouter
43 | */
44 | public function setSiteContext(SiteContext $siteContext)
45 | {
46 | $this->siteContext = $siteContext;
47 |
48 | return $this;
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function matchRequest(Request $request)
55 | {
56 | $match = parent::matchRequest($request);
57 | $this->setMatchContext($match);
58 |
59 | return $match;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function match($pathinfo)
66 | {
67 | $match = parent::match($pathinfo);
68 | $this->setMatchContext($match);
69 |
70 | return $match;
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
77 | {
78 | if (isset($parameters['_branding'])) {
79 | $branding = $parameters['_branding'];
80 | unset($parameters['_branding']);
81 | } else {
82 | $branding = $this->getSiteContext()->getCurrentBrandingName();
83 | }
84 |
85 | if (isset($parameters['_locale'])) {
86 | $locale = $parameters['_locale'];
87 | unset($parameters['_locale']);
88 | } else {
89 | $locale = $this->siteContext->getCurrentLocale();
90 | }
91 |
92 | if (null !== $branding && null !== $locale) {
93 | $multisiteName = self::ROUTE_PREFIX.'_'.$branding.'_'.$locale.'__'.$name;
94 | try {
95 | return parent::generate($multisiteName, $parameters, $referenceType);
96 | } catch (RouteNotFoundException $e) {
97 | // fallback to default behavior
98 | }
99 | }
100 |
101 | return parent::generate($name, $parameters, $referenceType);
102 | }
103 |
104 | /**
105 | * {@inheritdoc}
106 | */
107 | public function getRouteCollection()
108 | {
109 | $collection = parent::getRouteCollection();
110 | $routes = $collection->all();
111 | $newCollection = new RouteCollection();
112 |
113 | $routes = $this->sortRoutes($routes);
114 | foreach ($routes as $name => $route) {
115 | $newCollection->add($name, $route);
116 | }
117 |
118 | return $newCollection;
119 | }
120 |
121 | /**
122 | * Sort routes by domain.
123 | *
124 | * @param Route[] $routes
125 | *
126 | * @return Route[]
127 | */
128 | protected function sortRoutes(array $routes)
129 | {
130 | if (!$this->sortRoutes) {
131 | return $routes;
132 | }
133 |
134 | // group by host is a good-enough strategy for most of the cases
135 | $hosts = array();
136 | foreach ($routes as $name => $route) {
137 | $branding = $route->getDefault('_branding');
138 | $locale = $route->getDefault('_locale');
139 | $hosts[$branding.'__'.$locale][$name] = $route;
140 | }
141 |
142 | return call_user_func_array('array_merge', $hosts);
143 | }
144 |
145 | /**
146 | * @return SiteContext
147 | *
148 | * @throws RuntimeException If no site context is available
149 | */
150 | private function getSiteContext()
151 | {
152 | if (null === $this->siteContext) {
153 | throw new \RuntimeException('No site context injected in router.');
154 | }
155 |
156 | return $this->siteContext;
157 | }
158 |
159 | /**
160 | * Changes router context and site context according to matched route.
161 | *
162 | * @param array $match
163 | */
164 | private function setMatchContext(array $match)
165 | {
166 | if (isset($match['_branding'])) {
167 | $this->context->setParameter('_branding', $match['_branding']);
168 |
169 | $this->siteContext->setCurrentBranding($this->siteContext->getBranding($match['_branding']));
170 | }
171 |
172 | if (isset($match['_locale'])) {
173 | $this->context->setParameter('_locale', $match['_locale']);
174 |
175 | $this->siteContext->setCurrentLocale($match['_locale']);
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AlexMultisiteBundle - Branding and Internationalization
2 |
3 |  [](https://packagist.org/packages/alexandresalome/multisite-bundle) [](https://packagist.org/packages/alexandresalome/multisite-bundle) [](https://packagist.org/packages/alexandresalome/multisite-bundle) [](https://packagist.org/packages/alexandresalome/multisite-bundle) [](https://packagist.org/packages/alexandresalome/multisite-bundle)
4 |
5 | This bundle allows you to manage multiple brandings and multiple locales in a Symfony2 application.
6 |
7 | * [View slides](slides.pdf)
8 | * [CHANGELOG](CHANGELOG.md)
9 | * [CONTRIBUTORS](CONTRIBUTORS.md)
10 |
11 | **Requirements**:
12 |
13 | * FrameworkExtraBundle
14 | * TwigBundle
15 |
16 | ## Features
17 |
18 | * Multiple routes for each site
19 | * Configuration per site
20 | * Templates per site
21 |
22 | ## Installation
23 |
24 | Add to your **composer.json**:
25 |
26 | ```json
27 | {
28 | "require": {
29 | "alexandresalome/multisite-bundle": "~0.1"
30 | }
31 | }
32 | ```
33 |
34 | Add the bundle to your kernel:
35 |
36 | ```php
37 | # app/AppKernel.php
38 |
39 | class AppKernel extends Kernel
40 | {
41 | public function registerBundles()
42 | {
43 | $bundles = array(
44 | # ...
45 | new Alex\MultisiteBundle\AlexMultisiteBundle(),
46 | );
47 | }
48 | }
49 | ```
50 |
51 | ## Configuration
52 |
53 | Add this section to your **config.yml** file:
54 |
55 | ```yaml
56 | alex_multisite:
57 | default_branding: branding_A
58 | default_locale: fr_FR
59 | brandings:
60 | _defaults:
61 | register: true
62 | branding_A:
63 | en_GB: { host: branding-a.com }
64 | fr_FR: { host: branding-a.com, prefix: /fr }
65 | branding_B:
66 | _defaults:
67 | register: false
68 | en_GB: { host: branding-b.com }
69 | de_DE: { host: branding-b.de, register: false }
70 | ```
71 |
72 | In this section, you must configure your brandings and locales.
73 |
74 | You can also add extra options, like the **register** option here.
75 |
76 | ## Declare your routes
77 |
78 | In your controllers, substitute
79 |
80 | ```php
81 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
82 | ```
83 |
84 | with
85 |
86 | ```php
87 | use Alex\MultisiteBundle\Annotation\Route;
88 | ```
89 |
90 | You can then configure a multisite route in two ways:
91 |
92 | ```php
93 | /**
94 | * @Route(name="login", paths={
95 | * "fr_FR"="/connexion",
96 | * "en_GB"="/login"
97 | * })
98 | */
99 | public function loginAction()
100 | # ...
101 | ```
102 |
103 | The path will be the same for all brandings, but will be localized. If you
104 | want a different path for same locale in different sites:
105 |
106 | ```php
107 | /**
108 | * @Route(name="login", paths={
109 | * "branding_A"={
110 | * "fr_FR"="/connexion-on-A",
111 | * "en_GB"="/login-on-A",
112 | * },
113 | * "branding_B"={
114 | * "en_GB"="/login-on-B",
115 | * },
116 | * })
117 | */
118 | public function loginAction()
119 | # ...
120 | ```
121 |
122 | ## Override templates
123 |
124 | If you want to change a template for a specific site, create a similarly named file with branding/locale option in it:
125 |
126 | Given your default template is ``AcmeDemoBundle::contact.html.twig``.
127 |
128 | You can override it with branding, locale, or both:
129 |
130 | * ``AcmeDemoBundle::_branding_locale/contact.html.twig``
131 | * ``AcmeDemoBundle::_branding_/contact.html.twig``
132 | * ``AcmeDemoBundle::__locale/contact.html.twig``
133 |
134 | Just create the file and it will automatically be loaded in place of the previous one.
135 |
136 | ## Read the site context
137 |
138 | **From templates**, use the global variable **site_context**, which returns a ``Alex\MultisiteBundle\Branding\SiteContext`` instance:
139 |
140 | ```
141 | You are currently on {{ site_context.currentBrandingName }}
142 | Your locale is {{ site_context.currentLocale }}
143 | ```
144 |
145 | You can also read options from config with:
146 |
147 | ```
148 | The option register is {{ site_context.option('register') ? 'enabled': 'not enabled' }}
149 | ```
150 |
151 | **In your controllers**, use service **site_context**:
152 |
153 | ```php
154 | public function indexAction()
155 | {
156 | $this->get('site_context')->getCurrentLocale();
157 | $this->get('site_context')->getOption('register');
158 | }
159 | ```
160 |
161 | ## Disable route sorting
162 |
163 | You might want to rely on natural order of routes. If you're in this case, you can disable the optimization by providing the **sort_route** option:
164 |
165 | ```yaml
166 | alex_multisite:
167 | sort_routes: false
168 | # ...
169 | ```
170 |
171 | ## Security
172 |
173 | You can combine this bundle with Symfony's **SecurityBundle**, and use this bundle for your routes for login form page.
174 |
175 | But, you must **not** use the multisite feature for the following routes:
176 |
177 | * login-check
178 | * logout
179 |
--------------------------------------------------------------------------------
/Branding/SiteContext.php:
--------------------------------------------------------------------------------
1 | addBranding($branding);
48 | }
49 |
50 | $this->defaultBrandingName = $defaultBrandingName;
51 | $this->defaultLocale = $defaultLocale;
52 | }
53 |
54 | /**
55 | * Changes the current branding of site context.
56 | *
57 | * @param Branding $branding
58 | *
59 | * @return SiteContext
60 | */
61 | public function setCurrentBranding(Branding $branding)
62 | {
63 | $this->currentBranding = $branding;
64 |
65 | return $this;
66 | }
67 |
68 | /**
69 | * Changes the current locale.
70 | *
71 | * @param string $locale
72 | *
73 | * @return SiteContext
74 | */
75 | public function setCurrentLocale($locale)
76 | {
77 | $this->currentLocale = $locale;
78 |
79 | return $this;
80 | }
81 |
82 | /**
83 | * Returns name of the current branding.
84 | *
85 | * @return string
86 | */
87 | public function getCurrentBrandingName()
88 | {
89 | return $this->getCurrentBranding()->getName();
90 | }
91 |
92 | /**
93 | * Returns current branding.
94 | *
95 | * @return Branding
96 | */
97 | public function getCurrentBranding()
98 | {
99 | if (null === $this->currentBranding) {
100 | return $this->getBranding($this->defaultBrandingName);
101 | }
102 |
103 | return $this->currentBranding;
104 | }
105 |
106 | /**
107 | * Returns current locale.
108 | *
109 | * @return string
110 | */
111 | public function getCurrentLocale()
112 | {
113 | if (null === $this->currentLocale) {
114 | return $this->defaultLocale;
115 | }
116 |
117 | return $this->currentLocale;
118 | }
119 |
120 | /**
121 | * Returns a given branding.
122 | *
123 | * @param string $name
124 | *
125 | * @return Branding
126 | */
127 | public function getBranding($name)
128 | {
129 | foreach ($this->brandings as $branding) {
130 | if ($branding->getName() === $name) {
131 | return $branding;
132 | }
133 | }
134 |
135 | $names = array_map(function ($branding) {
136 | return $branding->getName();
137 | }, $this->brandings);
138 |
139 | throw new \InvalidArgumentException(sprintf('No branding named "%s". Available are: %s.', $name, implode(', ', $names)));
140 | }
141 |
142 | /**
143 | * Returns brandings with a given locale.
144 | *
145 | * @param string $locale
146 | *
147 | * @return Branding[]
148 | */
149 | public function getBrandingsWithLocale($locale)
150 | {
151 | $result = array();
152 | foreach ($this->brandings as $branding) {
153 | if ($branding->hasLocale($locale)) {
154 | $result[] = $branding;
155 | }
156 | }
157 |
158 | return $result;
159 | }
160 |
161 | /**
162 | * Converts an array used in annotation to an associative array branding/locale.
163 | *
164 | * @return array
165 | */
166 | public function normalizePaths(array $paths)
167 | {
168 | $result = array();
169 |
170 | foreach ($paths as $key => $value) {
171 | // key is locale
172 | if (is_string($value)) {
173 | foreach ($this->getBrandingsWithLocale($key) as $branding) {
174 | $result[$branding->getName()][$key] = $branding->prefixPath($key, $value);
175 | }
176 | }
177 |
178 | // key is branding
179 | if (is_array($value)) {
180 | foreach ($value as $locale => $path) {
181 | $result[$key][$locale] = $this->getBranding($key)->prefixPath($locale, $path);
182 | }
183 | }
184 | }
185 |
186 | return $result;
187 | }
188 |
189 | /**
190 | * Returns value of an option.
191 | *
192 | * @param string $name
193 | * @param mixed $default
194 | *
195 | * @return mixed
196 | */
197 | public function getOption($name, $default = null)
198 | {
199 | return $this->getCurrentBranding()->getOption($this->getCurrentLocale(), $name, $default);
200 | }
201 |
202 | /**
203 | * Adds a new branding to the context.
204 | *
205 | * @param Branding $branding
206 | *
207 | * @return SiteContext
208 | */
209 | private function addBranding(Branding $branding)
210 | {
211 | $this->brandings[] = $branding;
212 |
213 | return $this;
214 | }
215 | }
216 |
--------------------------------------------------------------------------------