Skip to content

[DI] Documenting Abstract Bundle and Extension #16801

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions bundles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,24 @@ The new bundle is called AcmeTestBundle, where the ``Acme`` portion is an exampl
name that should be replaced by some "vendor" name that represents you or your
organization (e.g. ABCTestBundle for some company named ``ABC``).

Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file
Start by creating a ``Acme/TestBundle/src/`` directory and adding a new file
called ``AcmeTestBundle.php``::

// src/Acme/TestBundle/AcmeTestBundle.php
namespace App\Acme\TestBundle;
// 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 ``AbstractBundle`` was introduced in Symfony 6.1. 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
Expand All @@ -74,7 +80,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.
Expand All @@ -86,28 +92,45 @@ 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 mainly 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``).

``Resources/public/``
``translations/``
Holds translations organized by domain and locale (e.g. ``AcmeTestBundle.en.xlf``).

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

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

A bundle can be as small or large as the feature it implements. It contains
only the files you need and nothing else.

Expand All @@ -126,3 +149,4 @@ Learn more
* :doc:`/bundles/prepend_extension`

.. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories
.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/
51 changes: 51 additions & 0 deletions bundles/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,57 @@ Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be
<!-- ... -->
</container>

Defining Configuration directly in your Bundle class
----------------------------------------------------

.. versionadded:: 6.1

The ``AbstractBundle`` class is introduced in Symfony 6.1.

As another option, you can define the extension configuration directly in your Bundle
class by implementing :class:`Symfony\\Component\\Config\\Definition\\ConfigurableInterface`,
which is already supported when your bundle extend from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle`::

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeFooBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void
{
// loads config definition from a file
$definition->import('../config/definition.php');

// loads config definition from multiple files (when it's too long you can split it)
$definition->import('../config/definition/*.php');

// if the configuration is short, consider adding it in this class
$definition->rootNode()
->children()
->scalarNode('foo')->defaultValue('bar')->end()
->end()
;
}
}

This method is a shortcut of the previous "Extension", "Configuration" and "TreeBuilder" convention,
now you also have the possibility to import configuration definition from an external file::

// Acme/FooBundle/config/definition.php
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

return static function (DefinitionConfigurator $definition) {
$definition->rootNode()
->children()
->scalarNode('foo')->defaultValue('bar')->end()
->end()
;
};

.. note::

The "configure()" method is called only at compile time.

.. _`FrameworkBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
.. _`TwigBundle Configuration`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php
.. _`XML namespace`: https://en.wikipedia.org/wiki/XML_namespace
Expand Down
41 changes: 41 additions & 0 deletions bundles/extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,44 @@ the full classmap executing the ``dump-autoload`` command of Composer.
This technique can't be used when the classes to compile use the ``__DIR__``
or ``__FILE__`` constants, because their values will change when loading
these classes from the ``classes.php`` file.

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
by extending from the :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 AcmeFooBundle extends AbstractBundle
{
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$container->parameters()
->set('foo', $config['foo']);

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

if ('bar' === $config['foo']) {
$container->services()
->set(Parser::class);
}
}
}

This method is a shortcut of the previous "load()" method, but with more options
to define and import the service configuration with less effort. The ``$config``
argument is the previous ``$configs`` array but already merged and processed. And
through the ``$container`` configurator you can import the services configuration
from an external file in any supported format (php, yaml, xml) or simply define
them in place using the fluent interfaces.

.. note::

The "loadExtension()", as the "load()" method, are called only at compile time.
18 changes: 9 additions & 9 deletions bundles/override.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http-kernel-resource-locator>`
to turn them into physical paths when needed.

Expand All @@ -23,12 +23,12 @@ Templates

Third-party bundle templates can be overridden in the
``<your-project>/templates/bundles/<bundle-name>/`` directory. The new templates
must use the same name and path (relative to ``<bundle>/Resources/views/``) as
must use the same name and path (relative to ``<bundle>/templates/``) as
the original templates.

For example, to override the ``Resources/views/Registration/confirmed.html.twig``
template from the FOSUserBundle, create this template:
``<your-project>/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig``
For example, to override the ``templates/registration/confirmed.html.twig``
template from the AcmeUserBundle, create this template:
``<your-project>/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig``

.. caution::

Expand All @@ -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 %}
...
Expand Down Expand Up @@ -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 ``<your-project>/translations/FOSUserBundle.es.yml`` file.
``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle,
create a ``<your-project>/translations/AcmeUserBundle.es.yaml`` file.

.. _`the Doctrine documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html#overrides
41 changes: 41 additions & 0 deletions bundles/prepend_extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,44 @@ More than one Bundle using PrependExtensionInterface
If there is more than one bundle that prepends the same extension and defines
the same key, the bundle that is registered **first** will take priority:
next bundles won't override this specific config setting.

Prepending Extension directly in your Bundle class
--------------------------------------------------

.. versionadded:: 6.1

The ``AbstractBundle`` class is introduced in Symfony 6.1.

By preference, you can append or prepend extension configuration directly in your Bundle
class for any bundle by extending from the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle`
and defining the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::prependExtension` method::

use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
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');
}
}

This method is a shortcut of the previous "PrependExtensionInterface::prepend" method,
allowing you also to import and append extension config from an external file in one of
the supported formats (php, yaml, xml).

.. note::

The "prependExtension()" like "prepend()" method is called only at compile time.
56 changes: 47 additions & 9 deletions configuration/micro_kernel_trait.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ that define your bundles, your services and your routes:
``RoutingConfigurator`` has methods that make adding routes in PHP more
fun. You can also load external routing files (shown below).

Advanced Example: Twig, Annotations and the Web Debug Toolbar
-------------------------------------------------------------
Advanced Example: Configuration, Twig, Annotations and the Web Debug Toolbar
----------------------------------------------------------------------------

The purpose of the ``MicroKernelTrait`` is *not* to have a single-file application.
Instead, its goal to give you the power to choose your bundles and structure.
Expand All @@ -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;
Expand All @@ -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');
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I'm not really sure if this is a good place to document AbstractExtension. What is its use-case when using MicroKernelTrait?

If we want to keep it, I would suggest moving the changes in this document to a new section below this one. Currently, I'm afraid the doc can make people think that they must use the app extension (or build() method) when using the micro kernel.

Copy link
Member Author

@yceruto yceruto May 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I'm not really sure if this is a good place to document AbstractExtension. What is its use-case when using MicroKernelTrait?

Apps on bundle-less approach still need to define custom configuration, and it's only possible through a new extension, even if the kernel class can implement the ExtensionInterface + Configuration it's better now by creating a new extension class and extending from AbstractExtension.

If we want to keep it, I would suggest moving the changes in this document to a new section below this one. Currently, I'm afraid the doc can make people think that they must use the app extension (or build() method) when using the micro kernel.

I think you're right, it could be better in a new section with the proper explanation.

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:

Expand Down Expand Up @@ -257,9 +297,7 @@ has one file in it::

class MicroController extends AbstractController
{
/**
* @Route("/random/{limit}")
*/
#[Route('/random/{limit}')]
public function randomNumber(int $limit): Response
{
$number = random_int(0, $limit);
Expand Down