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