Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple, dynamic database connections in Symfony2

Tags:

saas

symfony

I have a Symfony2 application that I want to make multi-tenant by the use of one database pr tenant (some don't consider this to be multi-tenancy, but that's not really the point).

The documentation describes how to accomplish this. However, I want to be able to create tenants dynamically, and writing the new database connection details (and entity managers) to the config.yml file directly seems messy. I would rather have a separate database which holds the tenants and their connections, and then select the proper connection/em based on an identifier (fetched, for instance, from a the subdomain of the app - clientname.app.com).

Using this approach I should be able to accomplish this, but will at the same time probably break the ability to specify the database connection and/or entity manager when running the command line commands for updating database schemas and the likes.

Provided that what I want to do make sense, is there a clever way to achieve this?

like image 372
Eirik A. Johansen Avatar asked Apr 02 '13 10:04

Eirik A. Johansen


1 Answers

I set ours up with a static database to handle login and tenancy information and a secondary database to hold user data

app/config/config.yml:

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
            tenantdb:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
    orm:
        default_entity_manager: default
        entity_managers:
            default:
                 connection: default
                 mappings:
                    MyCoreBundle: ~
            tenantdb:
                 connection: tenantdb
                 mappings:
                     MyAppBundle: ~

And then, in controllers, instead of

         $something = $this->getDoctrine()
                           ->getManager()
                           ->getRepository('MyAppBundle:Thing')
                           ->findAll();

we did:

         $something = $this->getDoctrine()
                           ->getManager('tenantdb')
                           ->getRepository('MyAppBundle:Thing', 'tenantdb')
                           ->findAll();

which you can find details of here: http://symfony.com/doc/current/cookbook/doctrine/multiple_entity_managers.html

Then, based on Symfony2, Dynamic DB Connection/Early override of Doctrine Service I set up a service to switch databases based on the subdomain of the request (e.g. tenant1.example.com tenant2.example.com)

src/MyCoreBundle/Resources/config/services.yml:

services:
    my.database_switcher:
        class: MyCoreBundle\EventListener\DatabaseSwitcherEventListener
        arguments:  [@request, @doctrine.dbal.tenantdb_connection]
        scope:      request
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

MyCoreBundle\EventListener\DatabaseSwitcherEventListener.php

namespace MyCoreBundle\EventListener;

use Symfony\Component\HttpFoundation\Request;
use Doctrine\DBAL\Connection;

class DatabaseSwitcherEventListener {

    private $request;
    private $connection;

    public function __construct(Request $request, Connection $connection) {
        $this->request = $request;
        $this->connection = $connection;
    }

    public function onKernelRequest() {
        $connection = $this->connection;
        if (! $connection->isConnected()) {
            $params = $this->connection->getParams();
            $subdomain = __GET_SUBDOMAIN__();
            $oldname = preg_replace (
                "/_tenant_$subdomain|_template/",
                '',
                $params['dbname']
            );
            $params['dbname'] =  $oldname . ($subdomain ? "_tenant_$subdomain"
                                                        : "_template");
            $connection->__construct(
                $params,
                $connection->getDriver(),
                $connection->getConfiguration(),
                $connection->getEventManager()
            );
            $connection->connect();
        }
    }

}

For convenience sake, we have an "extra" tenant database called XXX_template which system admins connect to when making global changes. The plan is that this database is copied to tenant databases on tenant create.

like image 183
Andy Preston Avatar answered Oct 13 '22 21:10

Andy Preston