diff --git a/bundles.rst b/bundles.rst index ed194614c34..81c21260032 100644 --- a/bundles.rst +++ b/bundles.rst @@ -46,20 +46,29 @@ Creating a Bundle This section creates and enables a new bundle to show there are only a few steps required. The new bundle is called AcmeTestBundle, where the ``Acme`` portion is an example name that should be replaced by some "vendor" name that represents you or your -organization (e.g. ABCTestBundle for some company named ``ABC``). +organization (e.g. AbcTestBundle for some company named ``Abc``). -Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file -called ``AcmeTestBundle.php``:: +Start by adding creating a new class called ``AcmeTestBundle``:: - // src/Acme/TestBundle/AcmeTestBundle.php - namespace App\Acme\TestBundle; + // src/AcmeTestBundle.php + namespace Acme\TestBundle; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class AcmeTestBundle extends Bundle + class AcmeTestBundle extends AbstractBundle { } +.. versionadded:: 6.1 + + The :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` was + introduced in Symfony 6.1. + +.. caution:: + + If your bundle must be compatible with previous Symfony versions you have to + extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` instead. + .. tip:: The name AcmeTestBundle follows the standard @@ -74,7 +83,7 @@ of the bundle. Now that you've created the bundle, enable it:: // config/bundles.php return [ // ... - App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], + Acme\TestBundle\AcmeTestBundle::class => ['all' => true], ]; And while it doesn't do anything yet, AcmeTestBundle is now ready to be used. @@ -86,35 +95,64 @@ The directory structure of a bundle is meant to help to keep code consistent between all Symfony bundles. It follows a set of conventions, but is flexible to be adjusted if needed: -``Controller/`` - Contains the controllers of the bundle (e.g. ``RandomController.php``). - -``DependencyInjection/`` - Holds certain Dependency Injection Extension classes, which may import service - configuration, register compiler passes or more (this directory is not - necessary). +``src/`` + Contains all PHP classes related to the bundle logic (e.g. ``Controller/RandomController.php``). -``Resources/config/`` +``config/`` Houses configuration, including routing configuration (e.g. ``routing.yaml``). -``Resources/views/`` - Holds templates organized by controller name (e.g. ``Random/index.html.twig``). +``templates/`` + Holds templates organized by controller name (e.g. ``random/index.html.twig``). + +``translations/`` + Holds translations organized by domain and locale (e.g. ``AcmeTestBundle.en.xlf``). -``Resources/public/`` +``public/`` Contains web assets (images, stylesheets, etc) and is copied or symbolically linked into the project ``public/`` directory via the ``assets:install`` console command. -``Tests/`` +``tests/`` Holds all tests for the bundle. -A bundle can be as small or large as the feature it implements. It contains -only the files you need and nothing else. +.. caution:: + + The recommended bundle structure was changed in Symfony 5, read the + `Symfony 4.4 bundle documentation`_ for information about the old + structure. + + When using the new ``AbstractBundle`` class, the bundle defaults to the + new structure. Override the ``Bundle::getPath()`` method to change to + the old structure:: + + class AcmeTestBundle extends AbstractBundle + { + public function getPath(): string + { + return __DIR__; + } + } + +.. tip:: -As you move through the guides, you'll learn how to persist objects to a -database, create and validate forms, create translations for your application, -write tests and much more. Each of these has their own place and role within -the bundle. + It's recommended to use the `PSR-4`_ autoload standard: use the namespace as key, + and the location of the bundle's main class (relative to ``composer.json``) + as value. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\TestBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\TestBundle\\Tests\\": "tests/" + } + } + } Learn more ---------- @@ -126,3 +164,5 @@ Learn more * :doc:`/bundles/prepend_extension` .. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories +.. _`Symfony 4.4 bundle documentation`: https://symfony.com/doc/4.4/bundles.html#bundle-directory-structure +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ diff --git a/bundles/configuration.rst b/bundles/configuration.rst index 1742457fb36..2f3d7268b19 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -315,6 +315,88 @@ In your extension, you can load this and dynamically set its arguments:: // ... now use the flat $config array } +Using the Bundle Class +---------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class is introduced in Symfony 6.1. + +Instead of creating an extension and configuration class, you can also +extend :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` to +add this logic to the bundle class directly:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->arrayNode('twitter') + ->children() + ->integerNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->end() + ->end() // twitter + ->end() + ; + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // Contrary to the Extension class, the "$config" variable is already merged + // and processed. You can use it directly to configure the service container. + $container->services() + ->get('acme.social.twitter_client') + ->arg(0, $config['twitter']['client_id']) + ->arg(1, $config['twitter']['client_secret']) + ; + } + } + +.. note:: + + The ``configure()`` and ``loadExtension()`` methods are called only at compile time. + +.. tip:: + + The ``AbstractBundle::configure()`` method also allows to import the + configuration definition from one or more files:: + + // src/AcmeSocialBundle.php + + // ... + class AcmeSocialBundle extends AbstractBundle + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/definition.php'); + // you can also use glob patterns + //$definition->import('../config/definition/*.php'); + } + + // ... + } + + .. code-block:: php + + // config/definition.php + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + + return static function (DefinitionConfigurator $definition) { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + }; + Modifying the Configuration of Another Bundle --------------------------------------------- diff --git a/bundles/extension.rst b/bundles/extension.rst index bbbfd398018..d05bf79a911 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -30,7 +30,7 @@ follow these conventions (but later you'll learn how to skip them if needed): This is how the extension of an AcmeHelloBundle should look like:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -87,7 +87,7 @@ but it is more common if you put these definitions in a configuration file (using the YAML, XML or PHP format). For instance, assume you have a file called ``services.xml`` in the -``Resources/config/`` directory of your bundle, your ``load()`` method looks like:: +``config/`` directory of your bundle, your ``load()`` method looks like:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -97,7 +97,7 @@ For instance, assume you have a file called ``services.xml`` in the { $loader = new XmlFileLoader( $container, - new FileLocator(__DIR__.'/../Resources/config') + new FileLocator(__DIR__.'/../../config') ); $loader->load('services.xml'); } @@ -111,6 +111,57 @@ The Extension is also the class that handles the configuration for that particular bundle (e.g. the configuration in ``config/packages/.yaml``). To read more about it, see the ":doc:`/bundles/configuration`" article. +Loading Services directly in your Bundle class +---------------------------------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class is introduced in Symfony 6.1. + +Alternatively, you can define and load services configuration directly in a +bundle class instead of creating a specific ``Extension`` class. You can do +this by extending from :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +and defining the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::loadExtension` +method:: + + // ... + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + class AcmeHelloBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + // load an XML, PHP or Yaml file + $container->import('../config/services.xml'); + + // you can also add or replace parameters and services + $container->parameters() + ->set('acme_hello.phrase', $config['phrase']) + ; + + if ($config['scream']) { + $container->services() + ->get('acme_hello.printer') + ->class(ScreamingPrinter::class) + ; + } + } + } + +This method works similar to the ``Extension::load()`` method, but it uses +a new API to define and import service configuration. + +.. note:: + + Contrary to the ``$configs`` parameter in ``Extension::load()``, the + ``$config`` parameter is already merged and processed by the + ``AbstractBundle``. + +.. note:: + + The ``loadExtension()`` is called only at compile time. + Adding Classes to Compile ------------------------- diff --git a/bundles/override.rst b/bundles/override.rst index bf53eb5ce3c..71bbe40ac7f 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -12,7 +12,7 @@ features of a bundle. The bundle overriding mechanism means that you cannot use physical paths to refer to bundle's resources (e.g. ``__DIR__/config/services.xml``). Always - use logical paths in your bundles (e.g. ``@FooBundle/Resources/config/services.xml``) + use logical paths in your bundles (e.g. ``@FooBundle/config/services.xml``) and call the :ref:`locateResource() method ` to turn them into physical paths when needed. @@ -23,12 +23,12 @@ Templates Third-party bundle templates can be overridden in the ``/templates/bundles//`` directory. The new templates -must use the same name and path (relative to ``/Resources/views/``) as +must use the same name and path (relative to ``/templates/``) as the original templates. -For example, to override the ``Resources/views/Registration/confirmed.html.twig`` -template from the FOSUserBundle, create this template: -``/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig`` +For example, to override the ``templates/registration/confirmed.html.twig`` +template from the AcmeUserBundle, create this template: +``/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig`` .. caution:: @@ -43,9 +43,9 @@ extend from the original template, not from the overridden one: .. code-block:: twig - {# templates/bundles/FOSUserBundle/Registration/confirmed.html.twig #} + {# templates/bundles/AcmeUserBundle/registration/confirmed.html.twig #} {# the special '!' prefix avoids errors when extending from an overridden template #} - {% extends "@!FOSUser/Registration/confirmed.html.twig" %} + {% extends "@!AcmeUser/registration/confirmed.html.twig" %} {% block some_block %} ... @@ -173,7 +173,7 @@ For this reason, you can override any bundle translation file from the main ``translations/`` directory, as long as the new file uses the same domain. For example, to override the translations defined in the -``Resources/translations/FOSUserBundle.es.yml`` file of the FOSUserBundle, -create a ``/translations/FOSUserBundle.es.yml`` file. +``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle, +create a ``/translations/AcmeUserBundle.es.yaml`` file. .. _`the Doctrine documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#overrides diff --git a/bundles/prepend_extension.rst b/bundles/prepend_extension.rst index c23f9133ff4..7e9f0195d8c 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -151,6 +151,45 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to 'use_acme_goodbye' => false, ]); +Prepending Extension in the Bundle Class +---------------------------------------- + +.. versionadded:: 6.1 + + The ``AbstractBundle`` class is introduced in Symfony 6.1. + +You can also append or prepend extension configuration directly in your +Bundle class if you extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class and define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::prependExtension` +method:: + + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + class FooBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + // prepend + $builder->prependExtensionConfig('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // append + $container->extension('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]) + + // append from file + $container->import('../config/packages/cache.php'); + } + } + +.. note:: + + The ``prependExtension()`` method, like ``prepend()``, is called only at compile time. + More than one Bundle using PrependExtensionInterface ---------------------------------------------------- diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst index 6a2c6a6375a..44b4c51b5d5 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -123,13 +123,15 @@ your ``composer.json`` file to load from there: Then, run ``composer dump-autoload`` to dump your new autoload config. -Now, suppose you want to use Twig and load routes via annotations. Instead of -putting *everything* in ``index.php``, create a new ``src/Kernel.php`` to -hold the kernel. Now it looks like this:: +Now, suppose you want to define a custom configuration for your app, +use Twig and load routes via annotations. Instead of putting *everything* +in ``index.php``, create a new ``src/Kernel.php`` to hold the kernel. +Now it looks like this:: // src/Kernel.php namespace App; + use App\DependencyInjection\AppExtension; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; @@ -146,13 +148,18 @@ hold the kernel. Now it looks like this:: new \Symfony\Bundle\TwigBundle\TwigBundle(), ]; - if ($this->getEnvironment() == 'dev') { + if ('dev' === $this->getEnvironment()) { $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); } return $bundles; } + protected function build(ContainerBuilder $container) + { + $container->registerExtension(new AppExtension()); + } + protected function configureContainer(ContainerConfigurator $c): void { $c->import(__DIR__.'/../config/framework.yaml'); @@ -205,6 +212,39 @@ Before continuing, run this command to add support for the new dependencies: $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle doctrine/annotations +Next, create a new extension class that defines your app configuration and +add a service conditionally based on the ``foo`` value:: + + // src/DependencyInjection/AppExtension.php + namespace App\DependencyInjection; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Extension\AbstractExtension; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + class AppExtension extends AbstractExtension + { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->booleanNode('foo')->defaultTrue()->end() + ->end(); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + if ($config['foo']) { + $container->set('foo_service', new \stdClass()); + } + } + } + +.. versionadded:: 6.1 + + The ``AbstractExtension`` class is introduced in Symfony 6.1. + Unlike the previous kernel, this loads an external ``config/framework.yaml`` file, because the configuration started to get bigger: