Slide 1

Slide 1 text

Demystifying Bundles

Slide 2

Slide 2 text

Lead Software Developer eos.uptrade GmbH [email protected] @jschaedl Jan Schädlich

Slide 3

Slide 3 text

@jschaedl Why a talk about Bundles?

Slide 4

Slide 4 text

@jschaedl What do we cover?

Slide 5

Slide 5 text

@jschaedl A little history

Slide 6

Slide 6 text

@jschaedl A little history 1 2 4 4.4 6.1

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

@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

Slide 9

Slide 9 text

@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

Slide 10

Slide 10 text

@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

Slide 11

Slide 11 text

@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

Slide 12

Slide 12 text

@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

Slide 13

Slide 13 text

@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 ‣ …

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

@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

Slide 16

Slide 16 text

// /src/SymfonyWorldBundle.php use Symfony\Component\HttpKernel\Bundle\Bundle; class SymfonyWorldBundle extends Bundle { }

Slide 17

Slide 17 text

// /config/bundles.php use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SymfonyWorldBundle\SymfonyWorldBundle; return [ FrameworkBundle::class => ['all' => true], SymfonyWorldBundle::class => ['all' => true], ];

Slide 18

Slide 18 text

// /config/bundles.php { "kernel.bundles": { "FrameworkBundle": "Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle", "SymfonyWorldBundle": "Symfony\\Bundle\\SymfonyWorldBundle\\SymfonyWorldBundle" } }

Slide 19

Slide 19 text

// /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); } }

Slide 20

Slide 20 text

// /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; }

Slide 21

Slide 21 text

// /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'; }

Slide 22

Slide 22 text

// /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

Slide 23

Slide 23 text

// /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')]); };

Slide 24

Slide 24 text

// /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; } }

Slide 25

Slide 25 text

// /src/DependencyInjection/Configuration.php symfony_world: edition: Summer year: 2022

Slide 26

Slide 26 text

// /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']); }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

> ./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 ---------------- -------------------------------------------------------

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

@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();

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

// vendor/symfony/http-kernel/Kernel.php public function handle(Request $request, ...): Response { // ... $this->boot(); // ... return $this->getHttpKernel()->handle($request, $type, $catch); }

Slide 34

Slide 34 text

@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()

Slide 35

Slide 35 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ └─ Kernel::initializeContainer() └─ SymfonyWorldBundle::boot()

Slide 36

Slide 36 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ └─ Kernel::initializeContainer() └─ SymfonyWorldBundle::boot()

Slide 37

Slide 37 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ │ └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() └─ SymfonyWorldBundle::boot()

Slide 38

Slide 38 text

// 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; } }

Slide 39

Slide 39 text

// src/Kernel.php use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel extends BaseKernel { use MicroKernelTrait; }

Slide 40

Slide 40 text

// 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

Slide 41

Slide 41 text

// 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'; }

Slide 42

Slide 42 text

// 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'; }

Slide 43

Slide 43 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ │ └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() └─ SymfonyWorldBundle::boot()

Slide 44

Slide 44 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ │ └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() │ ├─ Kernel::buildContainer() │ └─ ContainerBuilder::compile() └─ SymfonyWorldBundle::boot()

Slide 45

Slide 45 text

@jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ │ └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() │ ├─ Kernel::buildContainer() │ │ ├─ Kernel::prepareContainer() │ │ └─ Kernel::registerContainerConfiguration() │ └─ ContainerBuilder::compile() └─ SymfonyWorldBundle::boot()

Slide 46

Slide 46 text

@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()

Slide 47

Slide 47 text

// 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); } // ... }

Slide 48

Slide 48 text

// 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; }

Slide 49

Slide 49 text

// 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 ; }

Slide 50

Slide 50 text

// 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'; }

Slide 51

Slide 51 text

@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()

Slide 52

Slide 52 text

// vendor/symfony/http-kernel/Kernel.php protected function prepareContainer(ContainerBuilder $container) { // ... // register bundle extensions foreach ($this->bundles as $bundle) { $bundle->build($container); } // ... }

Slide 53

Slide 53 text

// 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()); }

Slide 54

Slide 54 text

@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()

Slide 55

Slide 55 text

// vendor/symfony/http-kernel/Kernel.php protected function prepareContainer(ContainerBuilder $container) { // register bundle extensions // call build() on every bundle $this->build($container); // ... }

Slide 56

Slide 56 text

// 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)) ; }

Slide 57

Slide 57 text

@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()

Slide 58

Slide 58 text

// vendor/symfony/http-kernel/KernelInterface.php interface KernelInterface extends HttpKernelInterface { /** * Loads the container configuration. */ public function registerContainerConfiguration(LoaderInterface $loader); // ... }

Slide 59

Slide 59 text

@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()

Slide 60

Slide 60 text

// 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'); } }

Slide 61

Slide 61 text

// 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'); } }

Slide 62

Slide 62 text

@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()

Slide 63

Slide 63 text

@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()

Slide 64

Slide 64 text

// 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); } // ... }

Slide 65

Slide 65 text

// 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)) ; }

Slide 66

Slide 66 text

@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()

Slide 67

Slide 67 text

// 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); }

Slide 68

Slide 68 text

// 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; }

Slide 69

Slide 69 text

// 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(

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

@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()

Slide 72

Slide 72 text

// /src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { dump($configs); // ... } }

Slide 73

Slide 73 text

// /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 ] ]

Slide 74

Slide 74 text

// /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); // ... } }

Slide 75

Slide 75 text

// /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 ]

Slide 76

Slide 76 text

// /src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { $config = $this->processConfiguration( new Configuration(), $configs ); dump($config); // ... } }

Slide 77

Slide 77 text

// ../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'])

Slide 78

Slide 78 text

// ../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')]); };

Slide 79

Slide 79 text

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']) ; } }

Slide 80

Slide 80 text

@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()

Slide 81

Slide 81 text

// 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 { // ... }

Slide 82

Slide 82 text

@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()

Slide 83

Slide 83 text

@jschaedl Extension points ‣ ExtensionInterface::load(array $con fi gs, ContainerBuilder $container) ‣ Bundle::build(ContainerBuilder $container) ‣ Bundle::boot()

Slide 84

Slide 84 text

@jschaedl Conventions

Slide 85

Slide 85 text

// 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

Slide 86

Slide 86 text

// 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

Slide 87

Slide 87 text

// 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

Slide 88

Slide 88 text

// /src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { // ... public function getAlias(): string { return 'sf_world'; } } // SymfonyWorldBundle: alias => sf_world

Slide 89

Slide 89 text

// 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 {

Slide 90

Slide 90 text

// 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.

Slide 91

Slide 91 text

// 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(); } }

Slide 92

Slide 92 text

// /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']) ; } }

Slide 93

Slide 93 text

// /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 ]

Slide 94

Slide 94 text

@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 …

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

// /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']))

Slide 97

Slide 97 text

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']) ; } // ... }

Slide 98

Slide 98 text

// /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'); } // ... }

Slide 99

Slide 99 text

// /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() ; };

Slide 100

Slide 100 text

// /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'); } // ... }

Slide 101

Slide 101 text

// /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() ; } // ...

Slide 102

Slide 102 text

// /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'; // ... }

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

@jschaedl When to use bundles?

Slide 105

Slide 105 text

@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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

No content