Multiple applications with Symfony2

As Symfony consultants, it’s a common request we have to split a Symfony2 project into multiple applications (à la symfony 1).

Even if Fabien does not seem to approve this usage, this is an easy and supported task.

Easy You may need a lightweight Kernel for some heavy tasks, you can need another web root and hostname for your administration or whatever bundle, although you only want one deployment, only one project.

Splitting your project in multiple one’s have a lot of implications. You will need new repositories for projects and shared core Bundles, two composer.lock which introduce a version mismatch risk, your deployment will get some more steps, your development environment will become more complex… This is not something all Symfony2 projects need or want. But splitting a project in multiple applications or Kernels can be helpful for smalls projects.

There are multiple benefits to split the Kernel:

  • hostname based routing / bundles / configuration, without mess (hostname_pattern parameter in the routing.yml file versus separated files);
  • no need to split your core Bundles in separated repositories (to use with composer and multiple projects organization);
  • only one deployment, that means no unsynchronised code / model / application versions, ever;
  • cleaner Kernel, you don’t need to load all the Bundles of your administration section (think about the ~8 Sonata Bundles you have) when hitting a front page;
  • you don’t have to mess up with your native environments and I really don’t recommend this.

How Symfony2 works out of the box

In the Symfony2 Standard Edition distribution, you have a default app/ directory, with an AppKernel.php file. This is the most important file of your project.

As you can guess by looking at the debug bar, app is your application name.

App is the Kernel name

Kernel gets his name from his current directory. The app directory is also the parent directory of your cache, logs, console, autoloading, configuration and PHPUnit files. By default, your Symfony2 project is a one app application.

The cache depends heavily on what the Kernel load, specially configuration. So you are not allowed to change what registerContainerConfiguration() loads based on some server variable or custom hostname checking other parameter than the Symfony environment if you don’t want your cache to become corrupted.

The web/ directory is where the public website files are stored, there is the app.php file, which load the AppKernel, and some other files not so important. It’s also where your Bundles public assets will be deployed, so this directory is dependant of the Kernel. 

Your code goes in /src and your dependencies in /vendor, two directories which belong to your whole project (they are not “application oriented”).

Multiple Kernels and web directories

For the purpose of this explanation, let’s say we want to split our front application and our API, but keep all the code in one project.

We want two different hostnames (api.pony.com & pony.com) and want to share:

  • database and core configurations;
  • some core bundles living in src/ (the one with our entities and business logic i.e.);
  • services and some global configuration;
  • vendors (only one composer.json).

but we do not want:

  • front assets in the api root;
  • useless bundles booted;
  • front routes in the api, kernel listeners;
  • same cache and logs…

To achieve this, we will create a new Kernel, a new web root and new configuration files.

The Kernel

To create our new Kernel (ApiKernel.php looks like a good name), we must move some files.

All files in the app/ folder can be moved to a new subfolder with a name of your choice, like pony:

    mkdir app/pony
    mv app/A* app/R* app/S* app/c* app/a* app/l* app/phpunit.xml.dist app/pony
    mkdir app/api
    cp -r app/pony/* app/api/
    mv app/pony/AppKernel.php app/pony/PonyKernel.php
    mv app/pony/AppCache.php app/pony/PonyCache.php
    mv app/api/AppKernel.php app/api/ApiKernel.php
    mv app/api/AppCache.php app/api/ApiCache.php
    find app/pony/* -type f -print | xargs sed -i 's/AppK/PonyK/g'
    find app/pony/* -type f -print | xargs sed -i 's/AppC/PonyC/g'
    find app/api/* -type f -print | xargs sed -i 's/AppK/ApiK/g'
    find app/api/* -type f -print | xargs sed -i 's/AppC/ApiC/g'

    As you can see, you have to edit those files: class AppKernel must be renamed to PonyKernel, or not – you are free to name it the way you like. The files under app/api/ are basically a copy of ones in app/pony/ but of course, we must change all occurrences of App* to Api*.

Here is what our ApiKernel class look like:

    use Symfony\Component\HttpKernel\Kernel;
    use Symfony\Component\Config\Loader\LoaderInterface;
    
    class ApiKernel extends Kernel
    {
        public function registerBundles()
        {
            $bundles = array(
                // Base Bundles, without Swift or Assetic as our API will not need them
                new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
                new Symfony\Bundle\SecurityBundle\SecurityBundle(),
                new Symfony\Bundle\TwigBundle\TwigBundle(),
                new Symfony\Bundle\MonologBundle\MonologBundle(),
                new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
                new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

                // Our core Bundle, with business logic and entities
                new Joli\CoreBundle\JoliCoreBundle(),
    
                // Our custom API Bundle
                new Joli\ApiBundle\JoliApiBundle(),
            );
    
            return $bundles;
        }
    
        public function registerContainerConfiguration(LoaderInterface $loader)
        {
            $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
        }
    }

We still load a file under a config subdirectory: app/api/config/config.yml. The pony application is doing the same but as we want to share common configuration instruction, we will create a new app/config folder (yes, that’s where our old configuration files were).

    mkdir app/config
    touch app/config/common_config.yml app/config/common_parameters.yml

All we have to do is importing those common configuration files:

    imports:
    - { resource: "../../config/common_parameters.yml" }
    - { resource: parameters.yml }
    - { resource: "../../config/common_config.yml" }

This way, our specific application parameters.yml file can override our common_parameters.yml values if needed, same for common_config.yml, and we do not duplicate any parameter.

One does not simply duplicate configuration

We now have two applications in our app/ directory, and the grammar nazi inside you is screaming. Lets rename it to apps.

mv app/ apps/

   

As Kernels now live in a pony and api directories, the web debug bar show us the new application names:

Pony in the debug bar

The web directory

Our web/ directory contains files that refer to our old app application. We have to move them to a web/pony subfolder and also create a web/api folder. We now have two DocumentRoot!

    mkdir web/pony
    mv web/app* web/.htaccess web/c* web/f* web/r* web/pony/
    mkdir web/api
    cp web/pony/app*.php web/pony/.htaccess web/api/

Then we edit our app.php files to match the new include path and Kernel name (you can refer to this commit).

The name of the app* files does not matter here. If you do not want to edit default .htaccess file, just keep a app.php file booting a PonyKernel. It’s your choice, just be sure to forward all the requests to the right file in your server configuration.

Composer

All our dependencies are in a shared vendors/ directory, so there is nothing to do for them. But look at the post-install scripts:

    "post-install-cmd": [
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
        "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile"
    ],

The first command builds a bootstrap.cache.php file in a default app directory. This file can be shared across our applications, it’s only a collection of common core classes, but we have to tell Symfony2 where to create it.  We could just edit the symfony-app-dir option at the end of your composer.json file and put apps in it, but what about other commands? They heavily rely on a Kernel to know which cache to clear, which assets to deploys, …  Composer scripts can’t be customized one by one, so we have to make a choice to get them working:

    "extra": {
        "symfony-app-dir": "apps/pony",
        "symfony-web-dir": "web/pony"
    }

Everything works well for the pony application, but the api application cache is not cleaned-up and the bootstrap file is not created. Cache is only a development issue, your deployment script must take care of this. But the bootstrap file should be shared across all our applications. Let’s symlink it!

    ln -s ./pony/bootstrap.php.cache apps/bootstrap.php.cache

Then simply put the right path in your web controllers files (web/pony/pony.php, etc…).

One more thing…

I could not leave you without a real working example to look at. And as code worth a thousand words, here is the Symfony Standard Multi Apps Edition! It a 2.2 based fork of the symfony-standard edition, on which I have applied all the needed modifications.

Keep in mind this will definitely change the way you work with your project. No more app/console: now you have to choose the application console between api and pony. Feel free also to move a maximum of your configurations to the “common” files, duplication is BAD.

Everything is not perfect (post-install composer scripts) and this may not be the ultimate-perfect-double-rainbow way of doing a multi-applications Symfony2 project but I think this can handle a large variety of needs without the pain of creating multiple Symfony2 projects. Feel free to comment or improve this solution!

blog comments powered by Disqus