From 995bd4fcbc7cdd05a63a1fe5dc521c013624bbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 17 Dec 2015 00:23:28 +0100 Subject: [PATCH] [DependencyInjection] Autowiring doc --- .../dependency_injection/autowiring.rst | 397 ++++++++++++++++++ components/dependency_injection/index.rst | 1 + components/map.rst.inc | 1 + 3 files changed, 399 insertions(+) create mode 100644 components/dependency_injection/autowiring.rst diff --git a/components/dependency_injection/autowiring.rst b/components/dependency_injection/autowiring.rst new file mode 100644 index 00000000000..86287d5d5a7 --- /dev/null +++ b/components/dependency_injection/autowiring.rst @@ -0,0 +1,397 @@ +.. index:: + single: DependencyInjection; Autowiring + +Defining Services Dependencies Automatically +============================================ + +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. + +Imagine you're building an API to publish statuses on a Twitter feed, obfuscated +with `ROT13`.. (a special case of the Caesar cipher). + +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 $transformer; + + public function __construct(Rot13Transformer $transformer) + { + $this->transformer = $transformer; + } + + public function tweet($user, $key, $status) + { + $transformedStatus = $this->transformer->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: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + twitter_client: + class: 'AppBundle\TwitterClient' + autowire: true + + .. code-block:: xml + + + + + + + + + + + .. 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 +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 +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')->tweet($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 + +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. It also allows to use other transformers. + +Let's introduce a ``TransformerInterface``:: + + // src/AppBundle/TransformerInterface.php + namespace AppBundle; + + interface TransformerInterface + { + public function transform($value); + } + +Then edit ``Rot13Transformer`` to make it implementing the new interface:: + + // ... + + class Rot13Transformer implements TransformerInterface + + // ... + + +And update ``TwitterClient`` to depend of this new interface:: + + class TwitterClient + { + // ... + + public function __construct(TransformerInterface $transformer) + { + // ... + } + + // ... + } + +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 + + + + + + + + + + + + .. 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 ``TransformerInterface`` 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 ``TransformerInterface`` +returning the result of the ROT13 transformation uppercased:: + + // src/AppBundle/UppercaseRot13Transformer.php + namespace AppBundle; + + class UppercaseTransformer implements TransformerInterface + { + private $transformer; + + public function __construct(TransformerInterface $transformer) + { + $this->transformer = $transformer; + } + + public function transform($value) + { + return strtoupper($this->transformer->transform($value)); + } + } + +This class is intended to decorate the any transformer and return its value 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)->tweet($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\TransformerInterface' + + 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 + + + + + + + + AppBundle\TransformerInterface + + + + + + + + + + .. code-block:: php + + use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\DependencyInjection\Definition; + + // ... + $definition1 = new Definition('AppBundle\Rot13Transformer'); + $definition1->setAutowiringTypes(array('AppBundle\TransformerInterface')); + $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 ``TransformerInterface``. +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\TransformerInterface" 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 +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 diff --git a/components/dependency_injection/index.rst b/components/dependency_injection/index.rst index 313f29af3f4..bc6ec0daf73 100644 --- a/components/dependency_injection/index.rst +++ b/components/dependency_injection/index.rst @@ -8,6 +8,7 @@ DependencyInjection types parameters definitions + autowiring synthetic_services compilation tags diff --git a/components/map.rst.inc b/components/map.rst.inc index dfead75f62c..64f2b8c70f2 100644 --- a/components/map.rst.inc +++ b/components/map.rst.inc @@ -46,6 +46,7 @@ * :doc:`/components/dependency_injection/types` * :doc:`/components/dependency_injection/parameters` * :doc:`/components/dependency_injection/definitions` + * :doc:`/components/dependency_injection/autowiring` * :doc:`/components/dependency_injection/synthetic_services` * :doc:`/components/dependency_injection/compilation` * :doc:`/components/dependency_injection/tags`