Decomposing Symfony Kernel: What does Minimal Symfony Bundle Do

Found a typo? Edit me

In the previous post, we looked at When Symfony Http Kernel is a Too Big Hammer to Use. We talked about the enormous content this package provides, but we don't need it.

Today we'll have a little self-reflecting pause in the middle of the 4-post journey. We'll look at the main glue in Symfony Kernel - the Bundle. Can we find a way to decompose it and use it without Kernel?

In the previous post, we described the problem: the Symfony Kernel requires a lot of http-related code that we don't need in command line applications. We still have to maintain it, require-dev dependencies in production, and handle their downgrade.

When a pandemic hit our world in 2020, we didn't think about our studies, where we would go for a vacation, or what car we'd buy. We care about our family, what we eat for dinner, and where we can buy everyday life essentials. When we find ourselves in times of trouble, we try to look at bare essentials.

What are the Kernel Essentials?

How can we use this approach in the "my application kernel is way too big" situation?


What do we need from Kernel?


How can Kernel do that?


What is the bundle class doing?


Which do we need from those 3?


1. The Bundle

Let's look at one of the Symplify bundles to demonstrate actual code. This is AstralBundle, which allows other kernels to register symplify/astral service and use them:

namespace Symplify\Astral\Bundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symplify\Astral\DependencyInjection\Extension\AstralExtension;

final class AstralBundle extends Bundle
{
    protected function createContainerExtension(): AstralExtension
    {
        return new AstralExtension();
    }
}

2. The Extension

The Bundle class contains createContainerExtension() that further creates AstralExtension. What is that extension doing?

namespace Symplify\Astral\DependencyInjection\Extension;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

final class AstralExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $containerBuilder): void
    {
        $phpFileLoader = new PhpFileLoader(
            $containerBuilder,
            new FileLocator(__DIR__ . '/../../config')
        );

        $phpFileLoader->load('config.php');
    }
}


It seems it loads the ../../config/config.php. There we define services and parameters.

So we need:

$fileLoader->load(__DIR__ . '/../../config.php');

Where Else can we Load Configs?

Does this look familiar? There are at least 2 places that handle the same operation. Nothing special, and they're pretty standard - you can see both in the code of this website.

One of them is MicroKernelTrait::configureContainer() method:

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;

final class TomasVotrubaKernel extends Kernel
{
    use MicroKernelTrait;

    protected function configureContainer(
        ContainerBuilder $containerBuilder,
        LoaderInterface $loader
    ): void {
        $loader->load(__DIR__ . '/../../vendor/symplify/astral/config/config.php');
    }
}


The other is in any config/config.php that we already load in our Kernel:

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(
        __DIR__ . '/../../vendor/symplify/astral/config/config.php'
    );
};

As you can see, we can replace Bundle + Extension classes with single-line import either in Kernel or in config.php. That's 2 classes and one method we don't have to worry about anymore.


Extension is gone, and we can import config elsewhere. What about the compiler passes? We can add them in the Kernel in build() method:

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    protected function build(ContainerBuilder $containerBuilder): void
    {
        $containerBuilder->addCompilerPass(new SomeCompilerPass());
    }
}

We've managed to tidy up the Kernel a bit. With less code to work with, we might see the solution more easily... or have we just moved the same problem to somewhere else?

We'll see about that next time.


Happy coding!


Have you find this post useful? Do you want more?

Follow me on Twitter, RSS or support me on GitHub Sponsors.