Skip to content
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

[DependencyInjection] Autowiring doc #6032

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
398 changes: 398 additions & 0 deletions components/dependency_injection/autowiring.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
.. index::
single: DependencyInjection; Autowiring

Defining Services Dependencies Automatically
============================================

Copy link
Member

Choose a reason for hiding this comment

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

versionadded directive should be added here

Autowiring allows to register services in the container with minimal configuration.
It is useful in the field of `Rapid Application Development`_, when designing prototypes
in early stages of large projects. It makes it easy to register a service graph
and eases refactoring.
Copy link
Member

Choose a reason for hiding this comment

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

I think we should mention what autowiring is here. E.g. start the paragraph with "Autowiring automatically resolves the service dependencies, based on the typehint of the constructor."


Imagine you're building an API to publish statuses on a Twitter feed, obfuscated
with `ROT13`.. (a special case of the Caesar cipher).
Copy link
Member

Choose a reason for hiding this comment

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

I'd remove the part about Caesar cipher and I'd add a link in ROT13 to https://en.wikipedia.org/wiki/ROT13

Copy link
Member Author

Choose a reason for hiding this comment

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

The link is already present. Why do you want to remove the part about the Caesar cipher?

Copy link
Member

Choose a reason for hiding this comment

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

.. has to be _


Start by creating a ROT13 transformer class::

// src/AppBundle/Rot13Transformer.php
namespace AppBundle;

class Rot13Transformer
{
public function transform($value)
{
return str_rot13($value);
}
}

And now a Twitter client using this transformer::

// src/AppBundle/TwitterClient.php
namespace AppBundle;

class TwitterClient
{
private $rot13Transformer;

public function __construct(Rot13Transformer $rot13Transformer)
{
$this->rot13Transformer = $rot13Transformer;
}

public function tweetInRot13($user, $key, $status)
{
$transformedStatus = $this->rot13Transformer->transform($status);

// ... connect to Twitter and send the encoded status
}
}

The Dependency Injection Component will be able to automatically register the dependencies
of this ``TwitterClient`` class by marking the ``twitter_client`` service as autowired:
Copy link
Member

Choose a reason for hiding this comment

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

Maybe use "when" instead of "by" here?


.. configuration-block::

.. code-block:: yaml

# app/config/services.yml
services:
twitter_client:
class: AppBundle\TwitterClient
Copy link
Contributor

Choose a reason for hiding this comment

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

single quotes?

Copy link
Member

Choose a reason for hiding this comment

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

not needed, it's Yaml... 😞

autowire: true

.. code-block:: xml

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="twitter_client" class="AppBundle\TwitterClient" autowire="true" />
</services>
</services>

.. code-block:: php

use Symfony\Component\DependencyInjection\Definition;

// ...
$definition = new Definition('AppBundle\TwitterClient');
$definition->setAutowired(true);

$container->setDefinition('twitter_client', $definition);

The autowiring subsystem will detect the dependencies of the ``TwitterClient``
class by parsing its constructor. For instance it will find here an instance of
a ``Rot13Transformer`` as dependency. If an existing service definition (and only
one – see below) is of the required type, this service will be injected. If it's
not the case (like in this example), the subsystem is smart enough to automatically
Copy link
Member

Choose a reason for hiding this comment

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

If it not the case ... -> If it's not the case ...

register a private service for the ``Rot13Transformer`` class and set it as first
argument of the `twitter_client`` service. Again, it can work only if there is one
Copy link
Contributor

Choose a reason for hiding this comment

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

missing a back tick (`)

class of the given type. If there are several classes of the same type, you must
use an explicit service definition or register a default implementation.

As you can see, the autowiring feature drastically reduces the amount of configuration
required to define a service. No more arguments section! It also makes it easy
to change the dependencies of the ``TwitterClient`` class: just add or remove typehinted
arguments in the constructor and you are done. There is no need anymore to search
and edit related service definitions.

Here is a typical controller using the ``twitter_client`` service::

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class DefaultController extends Controller
{
/**
* @Route("/tweet")
* @Method("POST")
*/
public function tweetAction(Request $request)
{
$user = $request->request->get('user');
$key = $request->request->get('key');
$status = $request->request->get('status');

if (!$user || !$key || !$status) {
throw new BadRequestHttpException();
}

$this->get('twitter_client')->tweetInRot13($user, $key, $status);

return new Response('OK');
}
}

You can give a try to the API with ``curl``::

curl -d "user=kevin&key=ABCD&status=Hello" http://localhost:8000/tweet
Copy link
Member

Choose a reason for hiding this comment

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

we should use .. code-block:: bash here and start the line with $


It should return ``OK``.

Working with Interfaces
-----------------------

You might also find yourself using abstractions instead of implementations (especially
in grown applications) as it allows to easily replace some dependencies without
modifying the class depending of them.

To follow this best practice, constructor arguments must be typehinted with interfaces
and not concrete classes. It allows to replace easily the current implementation
if necessary.

Let's introduce a ``Rot13TransformerInterface``::

// src/AppBundle/Rot13TransformerInterface.php
namespace AppBundle;

interface Rot13TransformerInterface
{
public function transform($value);
}

Then edit ``Rot13Transformer`` to make it implementing the new interface::

// ...

class Rot13Transformer implements Rot13TransformerInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

Rot13TransformerInterface doesn't make sense, why not just TransformerInterface. From my point of view interfaces shouldn't contain implementation details (rot13 in this case)


// ...


And update ``TwitterClient`` to depend of this new interface::

class TwitterClient
{
// ...

public function __construct(Rot13TransformerInterface $rot13Transformer)
{
// ...
}

// ...
}

Finally the service definition must be updated because, obviously, the autowiring
subsystem isn't able to find itself the interface implementation to register::

.. configuration-block::

.. code-block:: yaml

# app/config/services.yml
services:
rot13_transformer:
class: AppBundle\Rot13Transformer

twitter_client:
class: AppBundle\TwitterClient
autowire: true

.. code-block:: xml

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="rot13_transformer" class="AppBundle\Rot13Transformer" />
<service id="twitter_client" class="AppBundle\TwitterClient" autowire="true" />
</services>
</services>

.. code-block:: php

use Symfony\Component\DependencyInjection\Definition;

// ...
$definition1 = new Definition('AppBundle\Rot13Transformer');
$container->setDefinition('rot13_transformer', $definition1);

$definition2 = new Definition('AppBundle\TwitterClient');
$definition2->setAutowired(true);
$container->setDefinition('twitter_client', $definition2);

The autowiring subsystem detects that the ``rot13_transformer`` service implements
the ``Rot13TransformerInterface`` and injects it automatically. Even when using
interfaces (and you should), building the service graph and refactoring the project
is easier than with standard definitions.

Dealing with Multiple Implementations of the Same Type
------------------------------------------------------

Last but not least, the autowiring feature allows to specify the default implementation
of a given type. Let's introduce a new implementation of the ``Rot13TransformerInterface``
returning the result of the ROT13 transformation uppercased::

// src/AppBundle/UppercaseRot13Transformer.php
namespace AppBundle;

class UppercaseRot13Transformer implements Rot13TransformerInterface
{
private $rot13transformer;

public function __construct(Rot13TransformerInterface $rot13transformer)
{
$this->rot13transformer = $rot13transformer;
}

public function transform($value)
{
return strtoupper($this->rot13transformer->transform($value));
}
}

This class is intended to decorate the standard ROT13 transformer (or any other
implementation) and return it uppercased.

We can now refactor the controller to add another endpoint leveraging this new
transformer::

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class DefaultController extends Controller
{
/**
* @Route("/tweet")
* @Method("POST")
*/
public function tweetAction(Request $request)
{
return $this->tweet($request, 'twitter_client');
}

/**
* @Route("/tweet-uppercase")
* @Method("POST")
*/
public function tweetUppercaseAction(Request $request)
{
return $this->tweet($request, 'uppercase_twitter_client');
}

private function tweet(Request $request, $service)
{
$user = $request->request->get('user');
$key = $request->request->get('key');
$status = $request->request->get('status');

if (!$user || !$key || !$status) {
throw new BadRequestHttpException();
}

$this->get($service)->tweetInRot13($user, $key, $status);

return new Response('OK');
}
}

The last step is to update service definitions to register this new implementation
and a Twitter client using it::

.. configuration-block::

.. code-block:: yaml

# app/config/services.yml
services:
rot13_transformer:
class: AppBundle\Rot13Transformer
autowiring_types: AppBundle\Rot13TransformerInterface

twitter_client:
class: AppBundle\TwitterClient
autowire: true

uppercase_rot13_transformer:
class: AppBundle\UppercaseRot13Transformer
autowire: true

uppercase_twitter_client:
class: AppBundle\TwitterClient
arguments: [ @uppercase_rot13_transformer ]

.. code-block:: xml

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="rot13_transformer" class="AppBundle\Rot13Transformer">
<autowiring-type>AppBundle\Rot13TransformerInterface</autowiring-type>
</service>
<service id="twitter_client" class="AppBundle\TwitterClient" autowire="true" />
<service id="uppercase_rot13_transformer" class="AppBundle\UppercaseRot13Transformer" autowire="true" />
<service id="uppercase_twitter_client" class="AppBundle\TwitterClient">
<argument type="service" id="uppercase_rot13_transformer" />
</service>
</services>
</services>

.. code-block:: php

use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;

// ...
$definition1 = new Definition('AppBundle\Rot13Transformer');
$definition1->setAutowiringTypes(array('AppBundle\Rot13TransformerInterface'));
$container->setDefinition('rot13_transformer', $definition1);

$definition2 = new Definition('AppBundle\TwitterClient');
$definition2->setAutowired(true);
$container->setDefinition('twitter_client', $definition2);

$definition3 = new Definition('AppBundle\UppercaseRot13Transformer');
$definition3->setAutowired(true);
$container->setDefinition('uppercase_rot13_transformer', $definition3);

$definition4 = new Definition('AppBundle\TwitterClient');
$definition4->addArgument(new Reference('uppercase_rot13_transformer'));
$container->setDefinition('uppercase_twitter_client', $definition4);

It deserves some explanations. We now have 2 services implementing the ``Rot13TransformerInterface``.
The autowiring subsystem cannot guess which one to use, this leads to errors
like::

[Symfony\Component\DependencyInjection\Exception\RuntimeException]
Unable to autowire argument of type "AppBundle\Rot13TransformerInterface" for the service "twitter_client".

Fortunately, the ``autowiring_types`` key is here to specify which implementation
to use by default. This key can take a list of types if necessary (using a YAML
array).

Thanks to this setting, the ``rot13_transformer`` service is automatically injected
Copy link
Contributor

Choose a reason for hiding this comment

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

an argument of uppercase_rot13_transformerand twitter_client

Copy link
Member Author

Choose a reason for hiding this comment

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

done

as an argument of the ``uppercase_rot13_transformer`` and ``twitter_client`` services. For
the ``uppercase_twitter_client``, we use a standard service definition to inject
the specific ``uppercase_rot13_transformer`` service.

As for other RAD features such as the FrameworkBundle controller or annotations,
keep in mind to not use autowiring in public bundles nor in large projects with
complex maintenance needs.

.. _Rapid Application Development: https://en.wikipedia.org/wiki/Rapid_application_development
.. _ROT13: https://en.wikipedia.org/wiki/ROT13
Loading