Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

  2. Lead Software Developer eos.uptrade GmbH jschaedlich@eos-uptrade.de @jschaedl Jan Schädlich

  3. @jschaedl Why a talk about Bundles?

  4. @jschaedl What do we cover?

  5. @jschaedl A little history

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

  7. @jschaedl A little history Symfony 1 already provided a plugin

    system. 1 2 4 4.4 6.1
  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
  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
  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
  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
  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
  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 ‣ …
  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
  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
  16. // <bundle>/src/SymfonyWorldBundle.php use Symfony\Component\HttpKernel\Bundle\Bundle; class SymfonyWorldBundle extends Bundle { }

  17. // <app>/config/bundles.php use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SymfonyWorldBundle\SymfonyWorldBundle; return [ FrameworkBundle::class =>

    ['all' => true], SymfonyWorldBundle::class => ['all' => true], ];
  18. // <app>/config/bundles.php { "kernel.bundles": { "FrameworkBundle": "Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle", "SymfonyWorldBundle": "Symfony\\Bundle\\SymfonyWorldBundle\\SymfonyWorldBundle" }

    }
  19. // <bundle>/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); } }
  20. // <bundle>/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; }
  21. // <bundle>/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'; }
  22. // <bundle>/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
  23. // <bundle>/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')]); };
  24. // <bundle>/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; } }
  25. // <bundle>/src/DependencyInjection/Configuration.php symfony_world: edition: Summer year: 2022

  26. // <bundle>/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']); }
  27. > ./bin/console debug:container symfony_world.service

  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 ---------------- -------------------------------------------------------
  29. @jschaedl The Kernel boot process https://symfony.com/doc/current/introduction/http_fundamentals.html

  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();
  31. @jschaedl The Kernel boot process https://symfony.com/doc/current/introduction/http_fundamentals.html

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

  33. // vendor/symfony/http-kernel/Kernel.php public function handle(Request $request, ...): Response { //

    ... $this->boot(); // ... return $this->getHttpKernel()->handle($request, $type, $catch); }
  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()
  35. @jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ └─

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

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

    └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() └─ SymfonyWorldBundle::boot()
  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; } }
  39. // src/Kernel.php use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel

    extends BaseKernel { use MicroKernelTrait; }
  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
  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'; }
  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'; }
  43. @jschaedl └─ Kernel::boot() ├─ Kernel::preBoot() │ ├─ Kernel::initializeBundles() │ │

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

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

    └─ Kernel::registerBundles() │ └─ Kernel::initializeContainer() │ ├─ Kernel::buildContainer() │ │ ├─ Kernel::prepareContainer() │ │ └─ Kernel::registerContainerConfiguration() │ └─ ContainerBuilder::compile() └─ SymfonyWorldBundle::boot()
  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()
  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); } // ... }
  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; }
  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 ; }
  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'; }
  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()
  52. // vendor/symfony/http-kernel/Kernel.php protected function prepareContainer(ContainerBuilder $container) { // ... //

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

    extensions // call build() on every bundle $this->build($container); // ... }
  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)) ; }
  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()
  58. // vendor/symfony/http-kernel/KernelInterface.php interface KernelInterface extends HttpKernelInterface { /** * Loads

    the container configuration. */ public function registerContainerConfiguration(LoaderInterface $loader); // ... }
  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()
  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'); } }
  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'); } }
  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()
  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()
  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); } // ... }
  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)) ; }
  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()
  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); }
  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; }
  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(
  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
  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()
  72. // <bundle>/src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { public function load(array

    $configs, ContainerBuilder $container) { dump($configs); // ... } }
  73. // <bundle>/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 ] ]
  74. // <bundle>/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); // ... } }
  75. // <bundle>/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 ]
  76. // <bundle>/src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { public function load(array

    $configs, ContainerBuilder $container) { $config = $this->processConfiguration( new Configuration(), $configs ); dump($config); // ... } }
  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'])
  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']) // <bundle>/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')]); };
  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']) ; } }
  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()
  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 { // ... }
  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()
  83. @jschaedl Extension points ‣ ExtensionInterface::load(array $con fi gs, ContainerBuilder $container)

    ‣ Bundle::build(ContainerBuilder $container) ‣ Bundle::boot()
  84. @jschaedl Conventions

  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
  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 // <app>/config/packages/symfony_world.yaml symfony_world: edition: Summer year: 2022
  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 // <app>/config/packages/symfony_world.yaml symfony_world: edition: Summer year: 2022 // <app>/config/packages/sf_world.yaml sf_world: edition: Summer year: 2022
  88. // <bundle>/src/DependencyInjection/SymfonyWorldExtension.php class SymfonyWorldExtension extends Extension { // ... public

    function getAlias(): string { return 'sf_world'; } } // SymfonyWorldBundle: alias => sf_world
  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 {
  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.
  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(); } }
  92. // <bundle>/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']) ; } }
  93. // <bundle>/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 ]
  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 …
  95. @jschaedl ‣ Bundle must extend the new abstract class AbstractBundle

    Easier bundle con fi guration since Symfony 6.1
  96. // <bundle>/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']))
  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']) ; } // ... }
  98. // <bundle>/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'); } // ... }
  99. // <bundle>/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() ; };
  100. // <bundle>/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'); } // ... }
  101. // <bundle>/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() ; } // ...
  102. // <bundle>/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'; // ... }
  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

  104. @jschaedl When to use bundles?

  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
  106. Thank you! https://speakerdeck.com/jschaedl

  107. None