Jaa


Symfony2 multi tenant solution

Over the last month I've been busy developing a website hosted on Azure (PaaS) which was easier than expected to deploy.

Today I'd like to share a multitenant solution I developed for Symfony which came out very elegant and easy to use:

 

What I wanted was a way to include several customers under the same domain (and subdomain) with no passing of additional variables to control the current tenant over the different controllers. Specifically, what I wanted was a way to point to a tenant with the following structure:

 {domain.com}/{tenant}/{controller}

But of course that wasn't as easy as expected, so here is the code I used to make it work:

 app/config/routing.yml
 
 vendor_my:
 resource: "@VendorMyBundle/Controller/"
 type: annotation
 prefix: /{tenant}

I used FOSUserBundle, so it had to be configured too:

 app/config/security.yml
 
 firewalls:
 main:
 pattern: ^/
 form_login:
 provider: fos_userbundle
 login_path: fos_user_security_login
 check_path: fos_user_security_check
 csrf_provider: form.csrf_provider
 success_handler: my.login_success_handler
 default_target_path: administration
 anonymous: true
 logout:
 path: fos_user_security_logout
 target: fos_user_security_login
 
 access_control:
 - { path: .*/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
 - { path: .*/, role: ROLE_USER }

Note that the last two lines have some minor regex changes so it can accept any tenant

Then I had to include a EventListener to save the tenant configuration in the session after a user has successfully logged in

 /src/Vendor/MyBundle/EventListener/TenantListener.php
 
 namespace Vendor\MyBundle\EventListener;
 
 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 class TenantListener implements EventSubscriberInterface
 {
 private $defaultTenant;
 private $router;//add
 private $context;
 
 public function __construct($defaultTenant, \Symfony\Component\Routing\Router $router, \Symfony\Component\Security\Core\SecurityContext $context)
 {
 $this->defaultTenant = $defaultTenant;
 $this->router = $router;
 $this->context = $context;
 }
 
 public function onKernelRequest(GetResponseEvent $event)
 {
 $request = $event->getRequest();
 if($request->attributes->has('tenant')){
 $this->defaultTenant=$request->attributes->get('tenant');
 }
 if ($this->context->isGranted('ROLE_USER')||$this->context->isGranted('ROLE_ADMIN')) {
 $this->defaultTenant = $request->getSession()->get('tenant');
 } else {
 $request->getSession()->set('tenant', $this->defaultTenant);
 $request->attributes->set('tenant', $this->defaultTenant);
 }
 $this->router->getContext()->setParameter('tenant',$request->getSession()->get('tenant'));
 }
 
 public static function getSubscribedEvents()
 {
 return array(
 // must be registered before the default Locale listener
 KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
 );
 }
 }

And register the Listener under /app/config/config.yml

 services:
 vendor.tenant_listener:
 class: Vendor\MyBundle\EventListener\TenantListener
 arguments: ["%default_tenant%", @router, @security.context]
 tags:
 - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

With this solution I can pass the 'tenant' variable throughout the session so that every controller or twig can use it. Note that even if the user changes the tenant url, the variable considered is the one that was registered at the time of the login.

Now I can have a custom login theme for every tenant:

Extending the FOSUserBundle Controller in my Bundle:

 /src/Vendor/MyBundle/Controller/SecurityController.php
 
 namespace Vendor\MyBundle\Controller;
 
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use FOS\UserBundle\Controller\SecurityController as BaseController;
 use Symfony\Component\HttpFoundation\Request; 
 
 class SecurityController extends BaseController
 {
 
 protected function renderLogin(array $data)
 {
 $tenant = $this->container->get('request')->getSession()->get('tenant');
 
 ....

I can then check the tenant info passed in the Listener and use it to have a different experience for different tenants.

 

Great, right? Next post will be about checking if the current user belongs to the right tenant... To be continued