diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index dc7e4c5b390..12ebaf67eda 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -1,244 +1,389 @@ -How To Create Symfony Applications with Multiple Kernels -======================================================== +How to Create Multiple Symfony Applications with a Single Kernel +================================================================ -.. caution:: +In most Symfony applications, incoming requests are processed by the front controller at ``public/index.php``, which +instantiates the ``src/Kernel.php`` class to create the application kernel. This kernel loads the bundles, configurations, +and handles the request to generate the response. - Creating applications with multiple kernels is no longer recommended by - Symfony. Consider creating multiple small applications instead. +The current implementation of the Kernel class serves as a convenient default for a single application. However, it can +also manage multiple applications. While the Kernel typically runs the same application with different configurations +based on various :ref:`environments ` , it can be adapted to run different applications with +specific bundles and configurations. -In most Symfony applications, incoming requests are processed by the -``public/index.php`` front controller, which instantiates the ``src/Kernel.php`` -class to create the application kernel that loads the bundles and handles the -request to generate the response. +These are some of the common use cases for creating multiple applications with a single Kernel: -This single kernel approach is a convenient default, but Symfony applications -can define any number of kernels. Whereas -:ref:`environments ` run the same application with -different configurations, kernels can run different parts of the same -application. +* An application that defines an API can be divided into two segments to improve performance. The first segment serves + the regular web application, while the second segment exclusively responds to API requests. This approach requires + loading fewer bundles and enabling fewer features for the second part, thus optimizing performance; +* A highly sensitive application could be divided into two parts for enhanced security. The first part would only load + routes corresponding to the publicly exposed sections of the application. The second part would load the remainder of + the application, with its access safeguarded by the web server; +* A monolithic application could be gradually transformed into a more distributed architecture, such as micro-services. + This approach allows for a seamless migration of a large application while still sharing common configurations and + components. -These are some of the common use cases for creating multiple kernels: +Turning a Single Application into Multiple Applications +------------------------------------------------------- -* An application that defines an API could define two kernels for performance - reasons. The first kernel would serve the regular application and the second - one would only respond to the API requests, loading less bundles and enabling - less features; -* A highly sensitive application could define two kernels. The first one would - only load the routes that match the parts of the application exposed publicly. - The second kernel would load the rest of the application and its access would - be protected by the web server; -* A micro-services oriented application could define several kernels to - enable/disable services selectively turning a traditional monolith application - into several micro-applications. +Let's explore the steps required to convert a single application into a new one that supports multiple applications: -Adding a new Kernel to the Application --------------------------------------- +1. Create a new application; +2. Update the Kernel class to support multiple applications; +3. Add a new ``APP_ID`` environment variable; +4. Update the front controllers. -Creating a new kernel in a Symfony application is a three-step process: +The following example shows how to create a new application for the API of a new Symfony project. -1. Create a new front controller to load the new kernel; -2. Create the new kernel class; -3. Define the configuration loaded by the new kernel. +Step 1) Create a new Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The following example shows how to create a new kernel for the API of a given -Symfony application. +In this example, we will use the `Shared Kernel`_ pattern, where, although all applications maintain an isolated context, +they can share common bundles, configurations, and code if desired. The optimal approach will depend on your specific +needs and requirements, so it's up to you to decide which best suits your project. -Step 1) Create a new Front Controller -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +First, let's create a new ``apps`` directory at the root of your project, which will hold all the necessary applications. +Each application will follow a simplified directory structure like the one described in :ref:`Symfony Best Practice `: -Instead of creating the new front controller from scratch, it's easier to -duplicate the existing one. For example, create ``public/api.php`` from -``public/index.php``. +.. code-block:: text -Then, update the code of the new front controller to instantiate the new kernel -class instead of the usual ``Kernel`` class:: + your-project/ + ├─ apps/ + │ └─ api/ + │ ├─ config/ + │ │ ├─ bundles.php + │ │ ├─ routes.yaml + │ │ └─ services.yaml + │ └─ src/ + ├─ bin/ + │ └─ console + ├─ config/ + ├─ public/ + │ └─ index.php + ├─ src/ + │ └─ Kernel.php - // public/api.php - // ... - $kernel = new ApiKernel( - $_SERVER['APP_ENV'] ?? 'dev', - $_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev')) - ); - // ... +.. note:: + + Note that the ``config/`` and ``src/`` directories at the root of the project will represent the shared context among + all applications within the ``apps/`` directory. Therefore, you should carefully consider what is common and what + should be placed in the specific application. .. tip:: - Another approach is to keep the existing ``index.php`` front controller, but - add an ``if`` statement to load the different kernel based on the URL (e.g. - if the URL starts with ``/api``, use the ``ApiKernel``). + You might also consider renaming the namespace for the shared context, from ``App`` to ``Shared``, as it will make it + easier to distinguish and provide clearer meaning to this context. -Step 2) Create the new Kernel Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Since the new ``apps/api/src/`` directory will host the PHP code related to the API, we need to update the ``composer.json`` +file to include it in the autoload section: -Now you need to define the ``ApiKernel`` class used by the new front controller. -The easiest way to do this is by duplicating the existing ``src/Kernel.php`` -file and make the needed changes. +.. code-block:: json -In this example, the ``ApiKernel`` will load fewer bundles than the default -Kernel. Be sure to also change the location of the cache, logs and configuration -files so they don't collide with the files from ``src/Kernel.php``:: + { + "autoload": { + "psr-4": { + "Shared\\": "src/", + "Api\\": "apps/api/src/" + } + } + } - // src/ApiKernel.php - use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +Additionally, don't forget to run `composer dump-autoload` to generate the autoload files. - class ApiKernel extends Kernel +Step 2) Update the Kernel class to support Multiple Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since we aim to support multiple applications, we will add a new property ``string $id`` to the Kernel to identify the +application being loaded. This property will also allow us to split the cache, logs, and configuration files in order to +avoid collisions with other applications. Moreover, it contributes to performance optimization, as each application will +load only the required resources:: + + // src/Kernel.php + namespace Shared; + + // ... + + class Kernel extends BaseKernel { use MicroKernelTrait; - public function getProjectDir(): string + public function __construct(string $environment, bool $debug, private string $id) + { + parent::__construct($environment, $debug); + } + + public function getSharedConfigDir(): string + { + return $this->getProjectDir().'/config'; + } + + public function getAppConfigDir(): string + { + return $this->getProjectDir().'/apps/'.$this->id.'/config'; + } + + public function registerBundles(): iterable { - return \dirname(__DIR__); + $sharedBundles = require $this->getSharedConfigDir().'/bundles.php'; + $appBundles = require $this->getAppConfigDir().'/bundles.php'; + + // load common bundles, such as the FrameworkBundle, as well as + // specific bundles required exclusively for the app itself + foreach (array_merge($sharedBundles, $appBundles) as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } } public function getCacheDir(): string { - return $this->getProjectDir().'/var/cache/api/'.$this->environment; + // divide cache for each application + return ($_SERVER['APP_CACHE_DIR'] ?? $this->getProjectDir().'/var/cache').'/'.$this->id.'/'.$this->environment; } public function getLogDir(): string { - return $this->getProjectDir().'/var/log/api'; + // divide logs for each application + return ($_SERVER['APP_LOG_DIR'] ?? $this->getProjectDir().'/var/log').'/'.$this->id; } - protected function configureContainer(ContainerConfigurator $containerConfigurator): void + protected function configureContainer(ContainerConfigurator $container): void { - $containerConfigurator->import('../config/api/{packages}/*.yaml'); - $containerConfigurator->import('../config/api/{packages}/'.$this->environment.'/*.yaml'); - - if (is_file(\dirname(__DIR__).'/config/api/services.yaml')) { - $containerConfigurator->import('../config/api/services.yaml'); - $containerConfigurator->import('../config/api/{services}_'.$this->environment.'.yaml'); - } else { - $containerConfigurator->import('../config/api/{services}.php'); - } + // load common config files, such as the framework.yaml, as well as + // specific configs required exclusively for the app itself + $this->doConfigureContainer($container, $this->getSharedConfigDir()); + $this->doConfigureContainer($container, $this->getAppConfigDir()); } protected function configureRoutes(RoutingConfigurator $routes): void { - $routes->import('../config/api/{routes}/'.$this->environment.'/*.yaml'); - $routes->import('../config/api/{routes}/*.yaml'); - // ... load only the config routes strictly needed for the API + // load common routes files, such as the routes/framework.yaml, as well as + // specific routes required exclusively for the app itself + $this->doConfigureRoutes($routes, $this->getSharedConfigDir()); + $this->doConfigureRoutes($routes, $this->getAppConfigDir()); } - // If you need to run some logic to decide which bundles to load, - // you might prefer to use the registerBundles() method instead - private function getBundlesPath(): string + private function doConfigureContainer(ContainerConfigurator $container, string $configDir): void { - // load only the bundles strictly needed for the API - return $this->getProjectDir().'/config/api_bundles.php'; + $container->import($configDir.'/{packages}/*.{php,yaml}'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'); + + if (is_file($configDir.'/services.yaml')) { + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + } else { + $container->import($configDir.'/{services}.php'); + } + } + + private function doConfigureRoutes(RoutingConfigurator $routes, string $configDir): void + { + $routes->import($configDir.'/{routes}/'.$this->environment.'/*.{php,yaml}'); + $routes->import($configDir.'/{routes}/*.{php,yaml}'); + + if (is_file($configDir.'/routes.yaml')) { + $routes->import($configDir.'/routes.yaml'); + } else { + $routes->import($configDir.'/{routes}.php'); + } + + if (false !== ($fileName = (new \ReflectionObject($this))->getFileName())) { + $routes->import($fileName, 'annotation'); + } } } -.. versionadded:: 5.4 +In this example, we reuse the default implementation to import configuration and routes based on a given configuration +directory. As we saw earlier, this approach will import both shared and app-specific resources. - The ``getBundlesPath()`` method was introduced in Symfony 5.4. +Step 3) Add a new APP_ID environment variable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Step 3) Define the Kernel Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Now, let's introduce a new environment variable that identifies the current application. This new variable can be added +to the ``.env`` file to provide a default value, but it should typically be added to your web server configuration. -Finally, define the configuration files that the new ``ApiKernel`` will load. -According to the above code, this config will live in one or multiple files -stored in ``config/api/`` and ``config/api/ENVIRONMENT_NAME/`` directories. +.. code-block:: bash -The new configuration files can be created from scratch when you load only a few -bundles, because it will be small. Otherwise, duplicate the existing -config files in ``config/packages/`` or better, import them and override the -needed options. + # .env + APP_ID=api -Executing Commands with a Different Kernel ------------------------------------------- +.. caution:: -The ``bin/console`` script used to run Symfony commands always uses the default -``Kernel`` class to build the application and load the commands. If you need -to run console commands using the new kernel, duplicate the ``bin/console`` -script and rename it (e.g. ``bin/api``). + The value of this variable must match the application directory within ``apps/`` as it is used in the Kernel to load + the specific application configuration. -Then, replace the ``Kernel`` instance by your own kernel instance -(e.g. ``ApiKernel``). Now you can run commands using the new kernel -(e.g. ``php bin/api cache:clear``). +Step 4) Update the Front Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: +In this final step, we will update the front controllers ``public/index.php`` and ``bin/console`` to pass the value of +the ``APP_ID`` variable to the Kernel instance. This will allow the Kernel to load and run the specified application:: - The commands available for each console script (e.g. ``bin/console`` and - ``bin/api``) can differ because they depend on the bundles enabled for each - kernel, which could be different. + // public/index.php + use Shared\Kernel; + // ... + + return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $context['APP_ID']); + }; + +Similar to configuring the required ``APP_ENV`` and ``APP_DEBUG`` values, the third argument of the Kernel constructor +is now also necessary to setting the application ID, which is derived from an external configuration. + +For the second front controller, we will define a new console option to allow passing the application ID we want to run +under CLI context:: + + // bin/console + use Shared\Kernel; + // ... + + return function (InputInterface $input, array $context) { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $input->getParameterOption(['--id', '-i'], $context['APP_ID'])); + + $application = new Application($kernel); + $application->getDefinition() + ->addOption(new InputOption('--id', '-i', InputOption::VALUE_REQUIRED, 'The App ID')) + ; + + return $application; + }; + +That's it! + +Executing Commands +------------------ + +The ``bin/console`` script, which is used to run Symfony commands, always uses the ``Kernel`` class to build the +application and load the commands. If you need to run console commands for a specific application, you can provide the +``--id`` option along with the appropriate identity value: + +.. code-block:: terminal + + php bin/console cache:clear --id=api + // or + php bin/console cache:clear -iapi + + // alternatively + export APP_ID=api + php bin/console cache:clear + +You might want to update the composer auto-scripts section to run multiple commands simultaneously. In this example, +we assume you have a second application for managing the configuration (admin): + +.. code-block:: json + + { + "scripts": { + "auto-scripts": { + "cache:clear -iapi": "symfony-cmd", + "cache:clear -iadmin": "symfony-cmd", + "assets:install %PUBLIC_DIR% -iapi": "symfony-cmd", + "assets:install %PUBLIC_DIR% -iadmin --no-cleanup": "symfony-cmd" + } + } + } + +Then, run `composer auto-scripts` to test it! + +.. note:: -Rendering Templates Defined in a Different Kernel -------------------------------------------------- + The commands available for each console script (e.g. ``bin/console -iapi`` and ``bin/console -iadmin``) can differ + because they depend on the bundles enabled for each application, which could be different. -If you follow the Symfony Best Practices, the templates of the default kernel -will be stored in ``templates/``. Trying to render those templates in a -different kernel will result in a *There are no registered paths for namespace -"__main__"* error. +Rendering Templates +------------------- -In order to solve this issue, add the following configuration to your kernel: +Let's assume there is now another app called ``admin``. If you follow the :ref:`Symfony Best Practices `, the shared Kernel +templates will be located in the ``templates/`` directory at the project's root. For admin-specific templates, you can +create a new directory ``apps/admin/templates/`` which you will need to manually configure under the Admin application: .. code-block:: yaml - # config/api/twig.yaml + # apps/admin/config/packages/twig.yaml twig: paths: - # allows to use api/templates/ dir in the ApiKernel - "%kernel.project_dir%/api/templates": ~ + '%kernel.project_dir%/apps/admin/templates': Admin + +Then, use this Twig namespace to reference any template within the Admin application only, for example ``@Admin/form/fields.html.twig``. -Running Tests Using a Different Kernel --------------------------------------- +Running Tests +------------- -In Symfony applications, functional tests extend by default from the -:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` class. Inside that -class, a method called ``getKernelClass()`` tries to find the class of the kernel -to use to run the application during tests. The logic of this method does not -support multiple kernel applications, so your tests won't use the right kernel. +In Symfony applications, functional tests typically extend from the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` +class by default. Within its parent class, ``KernelTestCase``, there is a method called ``createKernel()`` that attempts to +create the kernel responsible for running the application during tests. However, the current logic of this method doesn't +include our new application ID argument, so we need to make an update:: -The solution is to create a custom base class for functional tests extending -from ``WebTestCase`` class and overriding the ``getKernelClass()`` method to -return the fully qualified class name of the kernel to use:: + // apps/api/tests/ApiTestCase.php + namespace Api\Tests; + use Shared\Kernel; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Symfony\Component\HttpKernel\KernelInterface; - // tests needing the ApiKernel to work, now must extend this - // ApiTestCase class instead of the default WebTestCase class class ApiTestCase extends WebTestCase { - protected static function getKernelClass() + protected static function createKernel(array $options = []): KernelInterface { - return 'App\ApiKernel'; + $env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test'; + $debug = $options['debug'] ?? (bool) ($_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true); + + return new Kernel($env, $debug, 'api'); } + } - // this is needed because the KernelTestCase class keeps a reference to - // the previously created kernel in its static $kernel property. Thus, - // if your functional tests do not run in isolated processes, a later run - // test for a different kernel will reuse the previously created instance, - // which points to a different kernel - protected function tearDown() - { - parent::tearDown(); +.. note:: + + Keep in mind that we will set a fixed application ID value in this instance, as the specific test cases extending + from ``ApiTestCase`` will focus solely on the ``api`` tests. + +In this situation, we have created a ``tests/`` directory inside the ``apps/api/`` application. As a result, we need to +inform both the ``composer.json`` file and our ``phpunit.xml`` configuration about its existence: - static::$class = null; +.. code-block:: json + + { + "autoload-dev": { + "psr-4": { + "Shared\\Tests\\": "tests/", + "Api\\Tests\\": "apps/api/tests/" + } } } -Adding more Kernels to the Application --------------------------------------- +Remember to run ``composer dump-autoload`` to generate the autoload files. + +And, here is the update needed for the ``phpunit.xml`` file: + +.. code-block:: xml -If your application is very complex and you create several kernels, it's better -to store them in their own directories instead of messing with lots of files in -the default ``src/`` directory: + + + tests + + + apps/api/tests + + + +Adding more Applications +------------------------ + +Now you can begin adding more applications as needed, such as an ``admin`` application to manage the project's +configuration and permissions. To do that, you will have to repeat the step 1 only: .. code-block:: text - project/ - ├─ src/ - │ ├─ ... - │ └─ Kernel.php - ├─ api/ - │ ├─ ... - │ └─ ApiKernel.php - ├─ ... - └─ public/ - ├─ ... - ├─ api.php - └─ index.php + your-project/ + ├─ apps/ + │ ├─ admin/ + │ │ ├─ config/ + │ │ │ ├─ bundles.php + │ │ │ ├─ routes.yaml + │ │ │ └─ services.yaml + │ │ └─ src/ + │ └─ api/ + │ └─ ... + +Additionally, you might need to update your web server configuration to set the ``APP_ID=admin`` under a different domain. + +.. _`Shared Kernel`: http://ddd.fed.wiki.org/view/shared-kernel