$30 off During Our Annual Pro Sale. View Details »

Demystifying Bundles

Demystifying Bundles

SymfonyWorld Online Summer Edition 2022

Using bundles is not recommended anymore since Symfony 4. But they are still an important building block within Symfony's eco-system. In this talk I want to show you how bundles work, what they can and should be used for and how you can utilize them in your application.

Jan Schädlich

June 16, 2022
Tweet

More Decks by Jan Schädlich

Other Decks in Programming

Transcript

  1. Demystifying


    Bundles

    View Slide

  2. Lead Software Developer


    eos.uptrade GmbH


    [email protected]


    @jschaedl
    Jan Schädlich

    View Slide

  3. @jschaedl
    Why a talk about Bundles?

    View Slide

  4. @jschaedl
    What do we cover?

    View Slide

  5. @jschaedl
    A little history

    View Slide

  6. @jschaedl
    A little history
    1 2 4 4.4 6.1

    View Slide

  7. @jschaedl
    A little history
    Symfony 1 already provided
    a plugin system.
    1 2 4 4.4 6.1

    View Slide

  8. @jschaedl
    A little history
    Symfony 1 already provided
    a plugin system.
    Symfony 2 came with the bundle system and
    a recommendation to even organise your
    application code using the AppBundle.
    1 2 4 4.4 6.1

    View Slide

  9. @jschaedl
    A little history
    Symfony 1 already provided
    a plugin system.
    Symfony 2 came with the bundle system and
    a recommendation to even organise your
    application code using the AppBundle.
    Organising application code by using
    bundles is not recommended anymore.
    1 2 4 4.4 6.1

    View Slide

  10. @jschaedl
    A little history
    Symfony 1 already provided
    a plugin system.
    Symfony 2 came with the bundle system and
    a recommendation to even organise your
    application code using the AppBundle.
    Organising application code by using
    bundles is not recommended anymore.
    The bundle folder structure was updated to
    follow the Symfony standard application.
    (The old structure is still supported)
    1 2 4 4.4 6.1

    View Slide

  11. @jschaedl
    A little history
    Symfony 1 already provided
    a plugin system.
    Symfony 2 came with the bundle system and
    a recommendation to even organise your
    application code using the AppBundle.
    Organising application code by using
    bundles is not recommended anymore.
    The bundle folder structure was updated to
    follow the Symfony standard application.
    (The old structure is still supported)
    Easier bundle extension and
    configuration was introduced.
    1 2 4 4.4 6.1

    View Slide

  12. @jschaedl
    What is a Bundle
    … on a higher level
    ‣ It is an extension point to Symfony


    ‣ It works like a “plugin” in other programming environments


    ‣ It can add new features to an existing Symfony application


    ‣ It can be shared by multiple Symfony applications

    View Slide

  13. @jschaedl
    What is a Bundle
    … on a lower level
    ‣ It can add/remove/manipulate services in Symfony's DI
    container based on bundle speci
    fi
    c con
    fi
    guration


    ‣ It can de
    fi
    ne routing con
    fi
    guration


    ‣ It can provide templates


    ‣ It can provide translations


    ‣ …

    View Slide

  14. @jschaedl
    What is a Bundle
    … on a lower level
    ‣ It can add/remove/manipulate services in Symfony's DI
    container based on bundle speci
    fi
    c con
    fi
    guration


    ‣ It can de
    fi
    ne routing con
    fi
    guration


    ‣ It can provide templates


    ‣ It can provide translations


    ‣ …
    /


    ├── config/


    ├── docs/


    │ └─ index.md


    ├── public/


    ├── src/


    │ ├── Controller/


    │ ├── DependencyInjection/


    │ └── AcmeBlogBundle.php


    ├── templates/


    ├── tests/


    ├── translations/


    ├── LICENSE


    └── README.md

    View Slide

  15. @jschaedl
    Building blocks
    ‣ All classes we need to create a bundle are provided by
    Symfony's HttpKernel component


    ‣ The Symfony Framework itself uses bundles to couple and
    provide functionality based on its components

    View Slide

  16. // /src/SymfonyWorldBundle.php


    use Symfony\Component\HttpKernel\Bundle\Bundle;


    class SymfonyWorldBundle extends Bundle


    {


    }

    View Slide

  17. // /config/bundles.php


    use Symfony\Bundle\FrameworkBundle\FrameworkBundle;


    use Symfony\Bundle\SymfonyWorldBundle\SymfonyWorldBundle;


    return [


    FrameworkBundle::class => ['all' => true],


    SymfonyWorldBundle::class => ['all' => true],


    ];

    View Slide

  18. // /config/bundles.php


    {


    "kernel.bundles": {


    "FrameworkBundle": "Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle",


    "SymfonyWorldBundle": "Symfony\\Bundle\\SymfonyWorldBundle\\SymfonyWorldBundle"


    }


    }

    View Slide

  19. // /src/SymfonyWorldService.php


    class SymfonyWorldService implements SymfonyWorldServiceInterface


    {


    public function __construct(


    private readonly Edition $edition,


    private readonly int $year


    ) {


    }


    public function show(): string


    {


    return sprintf('Symfony World %s Edition %d', $this->edition->value, $this->year);


    }


    }

    View Slide

  20. // /src/SymfonyWorldService.php


    class SymfonyWorldService implements SymfonyWorldServiceInterface


    {


    public function __construct(


    private readonly Edition $edition,


    private readonly int $year


    ) {


    }


    public function show(): string


    {


    return sprintf('Symfony World %s Edition %d', $this->edition->value, $this->year);


    }


    }
    interface SymfonyWorldServiceInterface


    {


    public function show(): string;


    }

    View Slide

  21. // /src/SymfonyWorldService.php


    class SymfonyWorldService implements SymfonyWorldServiceInterface


    {


    public function __construct(


    private readonly Edition $edition,


    private readonly int $year


    ) {


    }


    public function show(): string


    {


    return sprintf('Symfony World %s Edition %d', $this->edition->value, $this->year);


    }


    }
    interface SymfonyWorldServiceInterface


    {


    public function show(): string;


    }
    enum Edition: string


    {


    case SUMMER = 'Summer';


    case WINTER = 'Winter';


    }

    View Slide

  22. // /src/SymfonyWorldService.php


    class SymfonyWorldService implements SymfonyWorldServiceInterface


    {


    public function __construct(


    private readonly Edition $edition,


    private readonly int $year


    ) {


    }


    public function show(): string


    {


    return sprintf('Symfony World %s Edition %d', $this->edition->value, $this->year);


    }


    }
    interface SymfonyWorldServiceInterface


    {


    public function show(): string;


    }
    enum Edition: string


    {


    case SUMMER = 'Summer';


    case WINTER = 'Winter';


    }
    $service = new SymfonyWorldService(Edition::SUMMER, 2022);


    echo $service->show();


    // Symfony World Summer Edition 2022

    View Slide

  23. // /config/services.php


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


    return static function (ContainerConfigurator $container) {


    $services = $container->services();


    $services


    ->set(‘symfony_world.service’, SymfonyWorldService::class)


    ->private()


    ->args([abstract_arg('edition'), abstract_arg('year')]);


    };

    View Slide

  24. // /src/DependencyInjection/Configuration.php


    class Configuration implements ConfigurationInterface


    {


    public function getConfigTreeBuilder(): TreeBuilder


    {


    $treeBuilder = new TreeBuilder('symfony_world'); // bundle extension alias => symfony_world


    $rootNode = $treeBuilder->getRootNode();


    $rootNode->children()


    ->enumNode('edition')


    ->values(array_map(fn (Edition $edition) => $edition->value, Edition::cases()))


    ->end()


    ->scalarNode('year')->end()


    ->end()


    ;


    return $treeBuilder;


    }


    }

    View Slide

  25. // /src/DependencyInjection/Configuration.php


    symfony_world:


    edition: Summer


    year: 2022

    View Slide

  26. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 2).'/config'));


    $loader->load('services.php');


    $container->getDefinition('symfony_world.service')


    ->setArgument('$edition', Edition::from($config['edition']))


    ->setArgument('$year', $config['year']);


    }


    View Slide

  27. > ./bin/console debug:container symfony_world.service

    View Slide

  28. > ./bin/console debug:container symfony_world.service
    Information for Service "symfony_world.service"


    ==========================================


    ---------------- -------------------------------------------------------


    Option Value


    ---------------- -------------------------------------------------------


    Service ID symfony_world.service


    Class Symfony\Bundle\SymfonyWorldBundle\SymfonyWorldService


    Tags -


    Public no


    Synthetic no


    Lazy no


    Shared yes


    Abstract no


    Autowired no


    Autoconfigured no


    ---------------- -------------------------------------------------------

    View Slide

  29. @jschaedl
    The Kernel boot process
    https://symfony.com/doc/current/introduction/http_fundamentals.html

    View Slide

  30. @jschaedl
    The Kernel boot process
    https://symfony.com/doc/current/introduction/http_fundamentals.html
    $request = Request::createFromGlobals();


    $kernel = new Kernel('prod', false);


    $response = $kernel->handle($request);


    $response->send();

    View Slide

  31. @jschaedl
    The Kernel boot process
    https://symfony.com/doc/current/introduction/http_fundamentals.html

    View Slide

  32. @jschaedl
    The Kernel boot process
    https://symfony.com/doc/current/introduction/http_fundamentals.html

    View Slide

  33. // vendor/symfony/http-kernel/Kernel.php


    public function handle(Request $request, ...): Response


    {


    // ...




    $this->boot();




    // ...


    return $this->getHttpKernel()->handle($request, $type, $catch);


    }

    View Slide

  34. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    │ └─ MergeExtensionConfigurationPass::process()


    │ └─ SymfonyWorldBundleExtension::load()


    └─ SymfonyWorldBundle::boot()

    View Slide

  35. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ └─ Kernel::initializeContainer()


    └─ SymfonyWorldBundle::boot()

    View Slide

  36. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ └─ Kernel::initializeContainer()


    └─ SymfonyWorldBundle::boot()

    View Slide

  37. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    └─ SymfonyWorldBundle::boot()

    View Slide

  38. // vendor/symfony/http-kernel/Kernel.php


    protected function initializeBundles()


    {


    $this->bundles = [];


    foreach ($this->registerBundles() as $bundle) {


    $name = $bundle->getName();


    if (isset($this->bundles[$name])) {


    throw new \LogicException(


    sprintf('Trying to register ... same name "%s".', $name)


    );


    }


    $this->bundles[$name] = $bundle;


    }


    }

    View Slide

  39. // src/Kernel.php


    use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;


    use Symfony\Component\HttpKernel\Kernel as BaseKernel;


    class Kernel extends BaseKernel


    {


    use MicroKernelTrait;


    }

    View Slide

  40. // vendor/symfony/framwork-bundle/Kernel/MicroKernelTrait.php


    public function registerBundles(): iterable


    {


    $contents = require $this->getBundlesPath();


    foreach ($contents as $class => $envs) {


    if ($envs[$this->environment] ?? $envs['all'] ?? false) {


    yield new $class();


    }




    // src/Kernel.php


    View Slide

  41. // vendor/symfony/framwork-bundle/Kernel/MicroKernelTrait.php


    public function registerBundles(): iterable


    {


    $contents = require $this->getBundlesPath();


    foreach ($contents as $class => $envs) {


    if ($envs[$this->environment] ?? $envs['all'] ?? false) {


    yield new $class();


    }




    // src/Kernel.php


    private function getBundlesPath(): string


    {


    return $this->getConfigDir().'/bundles.php';


    }

    View Slide

  42. // vendor/symfony/framwork-bundle/Kernel/MicroKernelTrait.php


    use Symfony\Bundle\FrameworkBundle\FrameworkBundle;


    use Symfony\Bundle\SymfonyWorldBundle\SymfonyWorldBundle;


    return [


    FrameworkBundle::class => ['all' => true],


    SymfonyWorldBundle::class => ['all' => true],


    ];
    // vendor/symfony/framwork-bundle/Kernel/MicroKernelTrait.php


    private function getBundlesPath(): string


    {


    return $this->getConfigDir().'/bundles.php';


    }

    View Slide

  43. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    └─ SymfonyWorldBundle::boot()

    View Slide

  44. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  45. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  46. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  47. // vendor/symfony/http-kernel/Kernel.php


    /**


    * Prepares the ContainerBuilder before it is compiled.


    */


    protected function prepareContainer(ContainerBuilder $container)


    {


    $extensions = [];


    foreach ($this->bundles as $bundle) {


    if ($extension = $bundle->getContainerExtension()) {


    $container->registerExtension($extension);


    }


    // ...


    }





    View Slide

  48. // vendor/symfony/http-kernel/Bundle/Bundle.php


    public function getContainerExtension(): ?ExtensionInterface


    {


    if (null === $this->extension) {


    $extension = $this->createContainerExtension();


    if (null !== $extension) {




    // ...




    $this->extension = $extension;


    } else {


    $this->extension = false;


    }


    }


    return $this->extension ?: null;


    }

    View Slide

  49. // vendor/symfony/http-kernel/Bundle/Bundle.php


    public function getContainerExtension(): ?ExtensionInterface


    {


    if (null === $this->extension) {


    $extension = $this->createContainerExtension();


    if (null !== $extension) {




    // ...




    $this->extension = $extension;


    } else {


    $this->extension = false;


    }


    }


    return $this->extension ?: null;


    }
    // vendor/symfony/http-kernel/Bundle/Bundle.php


    protected function createContainerExtension(): ?ExtensionInterface


    {


    return class_exists($class = $this->getContainerExtensionClass())


    ? new $class()


    : null


    ;


    }

    View Slide

  50. // vendor/symfony/http-kernel/Bundle/Bundle.php


    public function getContainerExtension(): ?ExtensionInterface


    {


    if (null === $this->extension) {


    $extension = $this->createContainerExtension();


    if (null !== $extension) {




    // ...




    $this->extension = $extension;


    } else {


    $this->extension = false;


    }


    }


    return $this->extension ?: null;


    }
    // vendor/symfony/http-kernel/Bundle/Bundle.php


    protected function createContainerExtension(): ?ExtensionInterface


    {


    return class_exists($class = $this->getContainerExtensionClass())


    ? new $class()


    : null


    ;


    }
    // vendor/symfony/http-kernel/Bundle/Bundle.php


    protected function getContainerExtensionClass(): string


    {


    $basename = preg_replace('/Bundle$/', '', $this->getName());


    return $this->getNamespace().'\\DependencyInjection\\'.$basename.'Extension';


    }

    View Slide

  51. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  52. // vendor/symfony/http-kernel/Kernel.php


    protected function prepareContainer(ContainerBuilder $container)


    {


    // ...


    // register bundle extensions


    foreach ($this->bundles as $bundle) {


    $bundle->build($container);


    }


    // ...


    }

    View Slide

  53. // vendor/symfony/http-kernel/Bundle/Bundle.php


    /**


    * Builds the bundle.


    *


    * It is only ever called once when the cache is empty.


    *


    * This method can be overridden to register compiler passes,


    * other extensions, ...


    */


    public function build(ContainerBuilder $container): void


    {


    parent::build($container);


    $container->registerExtension(new SymfonyWorldExtension());


    }

    View Slide

  54. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  55. // vendor/symfony/http-kernel/Kernel.php


    protected function prepareContainer(ContainerBuilder $container)


    {


    // register bundle extensions


    // call build() on every bundle


    $this->build($container);


    // ...


    }

    View Slide

  56. // vendor/symfony/http-kernel/Kernel.php


    protected function prepareContainer(ContainerBuilder $container)


    {


    // register bundle extensions


    // call Bundle::build() on every bundle


    // call Kernel::build()


    foreach ($container->getExtensions() as $extension) {


    $extensions[] = $extension->getAlias();


    }


    // ensure these extensions are implicitly loaded


    $container


    ->getCompilerPassConfig()


    ->setMergePass(new MergeExtensionConfigurationPass($extensions))


    ;


    }

    View Slide

  57. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  58. // vendor/symfony/http-kernel/KernelInterface.php


    interface KernelInterface extends HttpKernelInterface


    {


    /**


    * Loads the container configuration.


    */


    public function registerContainerConfiguration(LoaderInterface $loader);


    // ...


    }

    View Slide

  59. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  60. // vendor/symfony/framework-bundle/Kernel/MicroKernelTrait.php


    private function configureContainer(


    ContainerConfigurator $container,


    LoaderInterface $loader,


    ContainerBuilder $builder


    ): void {


    $configDir = $this->getConfigDir();


    $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');


    }


    }

    View Slide

  61. // vendor/symfony/framework-bundle/Kernel/MicroKernelTrait.php


    private function configureContainer(


    ContainerConfigurator $container,


    LoaderInterface $loader,


    ContainerBuilder $builder


    ): void {


    $configDir = $this->getConfigDir();


    $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');


    }


    }

    View Slide

  62. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  63. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    └─ SymfonyWorldBundle::boot()

    View Slide

  64. // vendor/symfony/dependency-injection/Compiler/Compiler.php


    /**


    * Run the Compiler and process all Passes.


    */


    public function compile(ContainerBuilder $container)


    {


    // ...


    foreach ($this->passConfig->getPasses() as $pass) {


    $pass->process($container);


    }


    // ...


    }

    View Slide

  65. // vendor/symfony/dependency-injection/Compiler/Compiler.php


    /**


    * Run the Compiler and process all Passes.


    */


    public function compile(ContainerBuilder $container)


    {


    // ...


    foreach ($this->passConfig->getPasses() as $pass) {


    $pass->process($container);


    }


    // ...


    }
    protected function prepareContainer(ContainerBuilder $container)


    {


    // register bundle extensions


    // call Bundle::build() on every bundle


    // call Kernel::build()


    foreach ($container->getExtensions() as $extension) {


    $extensions[] = $extension->getAlias();


    }


    // ensure these extensions are implicitly loaded


    $container


    ->getCompilerPassConfig()


    ->setMergePass(new MergeExtensionConfigurationPass($extensions))


    ;


    }

    View Slide

  66. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    │ └─ MergeExtensionConfigurationPass::process()


    └─ SymfonyWorldBundle::boot()

    View Slide

  67. // vendor/symfony/http-kernel/DependencyInjection/MergeExtensionConfigurationPass.php


    public function process(ContainerBuilder $container)


    {


    foreach ($this->extensions as $extension) {


    if (!\count($container->getExtensionConfig($extension))) {


    $container->loadFromExtension($extension, []);


    }


    }


    parent::process($container);


    }

    View Slide

  68. // vendor/symfony/http-kernel/DependencyInjection/MergeExtensionConfigurationPass.php


    public function process(ContainerBuilder $container)


    {


    foreach ($this->extensions as $extension) {


    if (!\count($container->getExtensionConfig($extension))) {


    $container->loadFromExtension($extension, []);


    }


    }


    parent::process($container);


    }
    // vendor/symfony/dependency-injection/ContainerBuilder.php


    public function loadFromExtension(


    string $extension,


    array $values = null


    ): static {


    if ($this->isCompiled()) {


    throw new BadMethodCallException(


    'Cannot load from an extension on a compiled container.’


    );


    }


    $namespace = $this->getExtension($extension)->getAlias();


    $this->extensionConfigs[$namespace][] = $values ?? [];


    return $this;


    }

    View Slide

  69. // vendor/symfony/dependency-injection/Compiler/MergeExtensionConfigurationPass.php


    public function process(ContainerBuilder $container)


    {


    // ...




    foreach ($container->getExtensions() as $name => $extension) {




    if (!$config = $container->getExtensionConfig($name)) {


    continue;


    }


    $resolvingBag = $container->getParameterBag();


    // ... resolve config values using container parameters


    $config = $resolvingBag->resolveValue($config);


    $tmpContainer = new MergeExtensionConfigurationContainerBuilder(




    View Slide



  70. if (!$config = $container->getExtensionConfig($name)) {


    continue;


    }


    $resolvingBag = $container->getParameterBag();


    // ... resolve config values using container parameters


    $config = $resolvingBag->resolveValue($config);


    $tmpContainer = new MergeExtensionConfigurationContainerBuilder(


    $extension,


    $resolvingBag


    );


    // ...


    $extension->load($config, $tmpContainer);


    // ... merge $tmpContainer into actual $container




    View Slide

  71. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    │ └─ MergeExtensionConfigurationPass::process()


    │ └─ SymfonyWorldBundleExtension::load()


    └─ SymfonyWorldBundle::boot()

    View Slide

  72. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    dump($configs);


    // ...


    }


    }


    View Slide

  73. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    dump($configs);


    // ...


    }


    }


    array:1 [


    0 => array:2 [


    "edition" => "Summer"


    "year" => 2022


    ]


    ]

    View Slide

  74. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    dump($config);


    // ...


    }


    }


    View Slide

  75. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    dump($config);


    // ...


    }


    }


    array:2 [


    "edition" => "Summer"


    "year" => 2022


    ]

    View Slide

  76. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    new Configuration(),


    $configs


    );


    dump($config);


    // ...


    }


    }


    View Slide

  77. // ../src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 2).'/config'));


    $loader->load('services.php');


    $container


    ->getDefinition('symfony_world.service')


    ->setArgument('$edition', Edition::from($config['edition']))


    ->setArgument('$year', $config['year'])


    View Slide

  78. // ../src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 2).'/config'));


    $loader->load('services.php');


    $container


    ->getDefinition('symfony_world.service')


    ->setArgument('$edition', Edition::from($config['edition']))


    ->setArgument('$year', $config['year'])


    // /config/services.php


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


    return static function (ContainerConfigurator $container) {


    $services = $container->services();


    $services


    ->set('symfony_world.service', SymfonyWorldService::class)


    ->private()


    ->args([abstract_arg('edition'), abstract_arg('year')]);


    };

    View Slide

  79. class SymfonyWorldExtension extends Extension


    {


    public function load(array $configs, ContainerBuilder $container)


    {


    $config = $this->processConfiguration(


    $this->getConfiguration($configs, $container),


    $configs


    );


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 2).'/config'));


    $loader->load('services.php');


    $container


    ->getDefinition('symfony_world.service')


    ->setArgument('$edition', Edition::from($config['edition']))


    ->setArgument('$year', $config['year'])


    ;


    }


    }

    View Slide

  80. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    │ └─ MergeExtensionConfigurationPass::process()


    │ └─ SymfonyWorldBundleExtension::load()


    !" SymfonyWorldBundle::boot()

    View Slide

  81. // vendor/symfony/http-kernel/Bundle/Bundle.php


    /**


    * Boots the Bundle.


    *


    * The $this->container property is already populated.


    *


    * Unlike Bundle::build(), Bundle::boot() is called for every Kernel boot.


    */


    public function boot(): void


    {


    // ...


    }

    View Slide

  82. @jschaedl
    └─ Kernel::boot()


    ├─ Kernel::preBoot()


    │ ├─ Kernel::initializeBundles()


    │ │ └─ Kernel::registerBundles()


    │ └─ Kernel::initializeContainer()


    │ ├─ Kernel::buildContainer()


    │ │ ├─ Kernel::prepareContainer()


    │ │ │ ├─ SymfonyWorldBundle::getContainerExtension()


    │ │ │ ├─ SymfonyWorldBundle::build()


    │ │ │ ├─ Kernel::build()


    │ │ │ └─ SymfonyWorldBundleExtension::getAlias()


    │ │ └─ Kernel::registerContainerConfiguration()


    │ │ └─ MicroKernelTrait::configureContainer()


    │ └─ ContainerBuilder::compile()


    │ └─ Compiler::compile()


    │ └─ MergeExtensionConfigurationPass::process()


    │ └─ SymfonyWorldBundleExtension::load()


    └─ SymfonyWorldBundle::boot()

    View Slide

  83. @jschaedl
    Extension points
    ‣ ExtensionInterface::load(array $con
    fi
    gs, ContainerBuilder $container)


    ‣ Bundle::build(ContainerBuilder $container)


    ‣ Bundle::boot()

    View Slide

  84. @jschaedl
    Conventions

    View Slide

  85. // vendor/symfony/dependency-injection—kernel/Extension/Extension.php


    public function getAlias(): string


    {


    $className = static::class;


    if (!str_ends_with($className, 'Extension')) {


    throw new BadMethodCallException('...');


    }


    $classBaseName = substr(strrchr($className, '\\'), 1, -9);


    return Container::underscore($classBaseName);


    }


    // SymfonyWorldBundle: alias => symfony_world

    View Slide

  86. // vendor/symfony/dependency-injection—kernel/Extension/Extension.php


    public function getAlias(): string


    {


    $className = static::class;


    if (!str_ends_with($className, 'Extension')) {


    throw new BadMethodCallException('...');


    }


    $classBaseName = substr(strrchr($className, '\\'), 1, -9);


    return Container::underscore($classBaseName);


    }


    // SymfonyWorldBundle: alias => symfony_world
    // /config/packages/symfony_world.yaml


    symfony_world:


    edition: Summer


    year: 2022

    View Slide

  87. // vendor/symfony/dependency-injection—kernel/Extension/Extension.php


    public function getAlias(): string


    {


    $className = static::class;


    if (!str_ends_with($className, 'Extension')) {


    throw new BadMethodCallException('...');


    }


    $classBaseName = substr(strrchr($className, '\\'), 1, -9);


    return Container::underscore($classBaseName);


    }


    // SymfonyWorldBundle: alias => symfony_world
    // /config/packages/symfony_world.yaml


    symfony_world:


    edition: Summer


    year: 2022
    // /config/packages/sf_world.yaml


    sf_world:


    edition: Summer


    year: 2022

    View Slide

  88. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends Extension


    {


    // ...


    public function getAlias(): string


    {


    return 'sf_world';


    }


    }


    // SymfonyWorldBundle: alias => sf_world

    View Slide

  89. // vendor/symfony/http-kernel/Bundle/Bundle.php


    public function getContainerExtension(): ?ExtensionInterface


    {


    if (null === $this->extension) {


    $extension = $this->createContainerExtension();


    if (null !== $extension) {


    // ...


    // check naming convention


    $basename = preg_replace('/Bundle$/', '', $this->getName());


    $expectedAlias = Container::underscore($basename);


    if ($expectedAlias != $extension->getAlias()) {


    throw new \LogicException(sprintf('...', $expectedAlias, $extension->getAlias()));


    }


    $this->extension = $extension;


    } else {


    View Slide

  90. // vendor/symfony/http-kernel/Bundle/Bundle.php


    public function getContainerExtension(): ?ExtensionInterface


    {


    if (null === $this->extension) {


    $extension = $this->createContainerExtension();


    if (null !== $extension) {


    // ...


    // check naming convention


    $basename = preg_replace('/Bundle$/', '', $this->getName());


    $expectedAlias = Container::underscore($basename);


    if ($expectedAlias != $extension->getAlias()) {


    throw new \LogicException(sprintf('...', $expectedAlias, $extension->getAlias()));


    }


    $this->extension = $extension;


    } else {


    Users will expect the alias of the default extension of a bundle


    to be the underscored version of the bundle name. You can override


    Bundle::getContainerExtension() if you want to use another alias.


    View Slide

  91. // vendor/symfony/http-kernel/Bundle/Bundle.php


    to be the underscored version of the bundle name. You can override


    Bundle::getContainerExtension() if you want to use another alias.


    final class SymfonyWorldBundle extends Bundle


    {


    /**


    * Override this method in case your Extension does not follow


    * the naming convention.


    */


    public function getContainerExtension(): ?ExtensionInterface


    {


    return $this->extension ?? new SymfonyWorldExtension();


    }


    }

    View Slide

  92. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends ConfigurableExtension


    {


    protected function loadInternal(array $mergedConfig, ContainerBuilder $container)


    {


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/config'));


    $loader->load('services.php');


    $container


    ->getDefinition('sf_world.service')


    ->setArgument('$edition', Edition::from($mergedConfig['edition']))


    ->setArgument('$year', $mergedConfig['year'])


    ;


    }


    }


    View Slide

  93. // /src/DependencyInjection/SymfonyWorldExtension.php


    class SymfonyWorldExtension extends ConfigurableExtension


    {


    protected function loadInternal(array $mergedConfig, ContainerBuilder $container)


    {


    $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/config'));


    $loader->load('services.php');


    $container


    ->getDefinition('sf_world.service')


    ->setArgument('$edition', Edition::from($mergedConfig['edition']))


    ->setArgument('$year', $mergedConfig['year'])


    ;


    }


    }


    array:2 [


    "edition" => "Summer"


    "year" => 2022


    ]

    View Slide

  94. @jschaedl
    ‣ A Bundle class


    ‣ A Con
    fi
    guration class to de
    fi
    ne bundle con
    fi
    g


    ‣ An Extension class which extends from the base Extension class
    and implements the ExtensionInterface::load() method to
    process bundle con
    fi
    g and load service de
    fi
    nitions


    ‣ Follow the conventions
    Bundles prior Symfony 6.1 needed …

    View Slide

  95. @jschaedl
    ‣ Bundle must extend the new abstract class AbstractBundle
    Easier bundle con
    fi
    guration since Symfony 6.1

    View Slide

  96. // /src/SymfonyWorldBundle.php


    use Symfony\Component\DependencyInjection\ContainerBuilder;


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


    use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    public function loadExtension(


    array $config,


    ContainerConfigurator $container,


    ContainerBuilder $builder


    ): void {


    $container->import('../config/services.php');


    $container->services()


    ->get('sf_world.service')


    ->arg('$edition', Edition::from($config['edition']))


    View Slide

  97. use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    public function loadExtension(


    array $config,


    ContainerConfigurator $container,


    ContainerBuilder $builder


    ): void {


    $container->import('../config/services.php');


    $container->services()


    ->get('sf_world.service')


    ->arg('$edition', Edition::from($config['edition']))


    ->arg('$year', $config['year'])


    ;


    }



    // ...


    }

    View Slide

  98. // /src/SymfonyWorldBundle.php


    use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;


    use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    // ...


    public function configure(DefinitionConfigurator $definition): void


    {


    $definition->import('../config/definition.php');


    }


    // ...


    }

    View Slide

  99. // /config/definition.php


    use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;


    return static function (DefinitionConfigurator $definition) {


    $definition->rootNode()


    ->children()


    ->enumNode('edition')


    ->values(array_map(fn (Edition $edition) => $edition->value, Edition::cases()))


    ->end()


    ->scalarNode('year')


    ->end()


    ->end()


    ;


    };


    View Slide

  100. // /src/SymfonyWorldBundle.php


    use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;


    use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    // ...


    public function configure(DefinitionConfigurator $definition): void


    {


    $definition->import('../config/definition/*.php');


    }


    // ...


    }


    View Slide

  101. // /src/SymfonyWorldBundle.php


    use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;


    use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    // ...


    public function configure(DefinitionConfigurator $definition): void


    {


    $definition->rootNode()


    ->children()


    ->scalarNode('foo')->defaultValue('bar')->end()


    ->end()


    ;


    }


    // ...


    View Slide

  102. // /src/SymfonyWorldBundle.php


    use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;


    use Symfony\Component\HttpKernel\Bundle\AbstractBundle;


    class SymfonyWorldBundle extends AbstractBundle


    {


    // ...


    protected string $extensionAlias = 'sf_world';


    // ...


    }

    View Slide

  103. @jschaedl
    https://symfony.com/blog/new-in-symfony-6-1-simpler-bundle-extension-and-con
    fi
    guration
    https://github.com/symfony/symfony/pull/43701

    View Slide

  104. @jschaedl
    When to use bundles?

    View Slide

  105. @jschaedl
    Tipps
    ‣ Use the new bundle folder structure


    ‣ Follow the conventions


    ‣ Use the container con
    fi
    gurator to de
    fi
    ne your bundle services


    ‣ Checkout what famous bundles do and follow along

    View Slide

  106. Thank you!
    https://speakerdeck.com/jschaedl

    View Slide

  107. View Slide