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.
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.
Section intitulée how-symfony2-works-out-of-the-boxHow 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.
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”).
Section intitulée multiple-kernels-and-web-directoriesMultiple 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.
Section intitulée the-kernelThe 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.
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:
Section intitulée the-web-directoryThe 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.
Section intitulée composerComposer
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…).
Section intitulée one-more-thingOne 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!
Commentaires et discussions
Ces clients ont profité de notre expertise
Nous avons développé une plateforme de site génériques autour de l’API Phraseanet. À l’aide de Silex de composants Symfony2, nous avons accompagné Alchemy dans la réalisation d’un site déclinable pour leurs clients. Le produit est intégralement configurable et supporte de nombreux systèmes d’authentification (Ldap, OAuth2, Doctrine ou anonyme).
Nous avons développé un outil statistique complet développé pour ORPI. Basé sur PHP, Symfony et Elasticsearch, cet outil offre à toutes les agences du réseau une visibilité accrue sur leurs annonces. Il garantit également une transparence totale envers les clients, en fournissant des statistiques détaillées sur les visualisations et les contacts de…
Afin de soutenir le développement de son trafic, Qobuz a fait appel à JoliCode afin d’optimiser l’infrastructure technique du site et les échanges d’informations entre les composants de la plateforme. Suite à la mise en place de solution favorisant l’asynchronicité et la performance Web côté serveur, nous avons outillé la recherche de performance et…