Introducing Light Kernel for Symfony Console Apps

In the first post of this miniseries, we look at Symfony Http Kernel with a critical eye on how it causes project overweight.

In the second post, we looked at bundles from a very raw point of view - what do we need from them?

In the spirit of thesis, antithesis, and synthesis philosophy, today, we'll combine both parts. We'll look for a solution to the original question: How can we build Kernel in Console Application without the Http burden?

Proof over theory? ECS, Monorepo Builder, EasyCI, Config Transformer and Rector are using this method since 1st November 2021. ECS is now 40 000 lines lighter, while keeping all the features running.
"Perfection is achieved, not when there is nothing more to add,
but when there is nothing left to take away."
Antoine de Saint-Exupery

Striving for Simplicity

In previous posts, we defined requirements that we want from Symfony Kernel from Console Applications:

What do we Have?

Currently, we have old projects that use symfony/http-kernel with a bunch of bundles. But when we look closer at Symfony bundles, we'll see they only add configs and compiler passes. So we can drop bundles and extensions altogether.

What do we Want?

Drop dependency on symfony/http-kernel, but make the project work as before.


In an ideal world, we want a container factory class that loads provided configs:

use Symfony\Component\DependencyInjection\ContainerBuilder;

final class ContainerBuilderFactory
{
    /**
     * @param string[] $configFiles
     * @param CompilerPassInterface[] $compilerPasses
     * @param ExtensionInterface[] $extensions
     */
    public function create(
        array $configFiles,
        array $compilerPasses,
        array $extensions
    ): ContainerBuilder {
        $containerBuilder = new ContainerBuilder();

        foreach ($extensions as $extension) {
            $containerBuilder->registerExtension($extension);
        }

        foreach ($configFiles as $configFile) {
            $delegatingLoader->load($configFile);
        }

        foreach ($compilerPasses as $compilerPass) {
            $containerBuilder->addCompilerPass($compilerPass);
        }

        return $containerBuilder;
    }
}

Then we could use it directly in any command-line application Kernel:

use Psr\Container\ContainerInterface;
use Symplify\SymplifyKernel\ContainerBuilderFactory;
use Symplify\ComposerJsonManipulator\ValueObject\ComposerJsonManipulatorConfig;

final class MonorepoBuilderKernel
{
    /**
     * @param string[] $configFiles
     */
    public function createFromConfigs(array $configFiles): ContainerInterface
    {
        // provide local config here
        $configFiles[] = __DIR__ . '/../../config/config.php';

        // external configs
        $configFiles[] = ComposerJsonManipulatorConfig::FILE_PATH;

        $containerBuilderFactory = new ContainerBuilderFactory();

        $containerBuilder = $containerBuilderFactory->create($configFiles, [], []);

        // build the container
        $containerBuilder->compile();

        return $containerBuilder;
    }
}

How would it meet our requirements?

How to use ContainerBuilderFactory in 3 Steps

For a couple of years, the Symplify uses own Symfony Kernel wrapper package - the symplify/symplify-kernel. It abstracts repeated methods and eases testing. What better place we could use for adding a dependency container factory?


1. Install Symplify Kernel

composer require symplify/symplify-kernel

2. Extend AbstractSymplifyKernel and provide config files

This the full kernel for the EasyCI package looks like:

namespace Symplify\EasyCI\Kernel;

use Psr\Container\ContainerInterface;
use Symplify\Astral\ValueObject\AstralConfig;
use Symplify\ComposerJsonManipulator\ValueObject\ComposerJsonManipulatorConfig;
use Symplify\SymplifyKernel\HttpKernel\AbstractSymplifyKernel;

final class EasyCIKernel extends AbstractSymplifyKernel
{
    /**
     * @param string[] $configFiles
     */
    public function createFromConfigs(array $configFiles): ContainerInterface
    {
        $configFiles[] = __DIR__ . '/../../config/config.php';
        $configFiles[] = ComposerJsonManipulatorConfig::FILE_PATH;
        $configFiles[] = AstralConfig::FILE_PATH;

        return $this->create([], [], $configFiles);
    }
}

3. Boot Kernel in your bin file and enjoy the Symfony DI

$easyCIKernel = new EasyCIKernel();
$easyCIKernel->createFromConfigs([__DIR__ . '/config/config.php']);

$container = $easyCIKernel->getContainer();

/** @var Application $application */
$application = $container->get(Application::class);
exit($application->run());

That's it! In the end, the setup is very simple.

It allowed us to drop the following files from 4 Simplify CLI packages:

Less code to transfer, faster CI pipelines, and environment-friendly code.


What would be even better? If Symfony core would provide a similar factory out of the box. Maybe one day, it will.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!