⬆️ ⬇️

Guide to using Dependency Injection in symfony2

This article provides an example of creating a simple blog site using the Dependency Injection pattern. A dependency-injection approach is applied to all possible symfony components: controllers, doctrine repositories, forms.



To simplify the article, we reduce the number of pages to two:



The final architecture of the application will look like this:





Step 1. Creating an arbitrary service



DI as part of symfony has already been considered on the habre, and also described in detail in the documentation . Therefore, we will immediately begin to create our own services and dependencies. This can be done in three ways: specifying dependencies in the bundle code, through configuration files (YAML, XML, PHP) and using annotations (using the JMSDiExtraBundle bundle that comes standard with Symfony). Each method has its pros and cons. We will use annotations for clarity and reduce the amount of code. Let's start with a class that implements business logic. Let it be a PostManager that handles the addition of a new post:

/src/AppBundle/Manager/PostManager.php
<?php namespace AppBundle\Manager; use JMS\DiExtraBundle\Annotation as DI; use AppBundle\Entity\Post; use AppBundle\Entity\User; /** * @DI\Service("app.manager.post", public=false) */ class PostManager { /** * @DI\Inject("doctrine.orm.entity_manager") * @var \Doctrine\ORM\EntityManager */ public $em; public function addPost(Post $post, User $user) { $post->setAuthor($user); $this->em->persist($post); $user->setLastPost($post); $user->increasePostsCount(); $this->em->flush(); } } 




@DI \ Service - turns the class into a service. The annotation parameters specify the name of the service (app.manager.post) and its attributes.

public = false - this attribute indicates that the created service cannot be called directly from DIC ($ container-> get ('app.manager.post') will result in an error). The created service will be able to use only services that depend on it explicitly (hereinafter, in the example with the controller, it will become clearer).

@DI \ Inject - an indication of the services on which the created service depends. This annotation can only be used with public variables. For private / protected dependency variables, you can use @DI \ InjectParams for the constructor or other ways to create services.



So, we have created the app.manager.post service, which depends on doctrine.orm.entity_manager :



A graphical display of services and links is available in a convenient web interface with the installation of the JMSDebuggingBundle .

')

Step 2. Creating a controller



By default, symfony controllers are not services, but there is a note in the documentation that allows them to be made. Create a PostController using the previously created PostManager:

/src/AppBundle/Controller/PostController.php
 <?php namespace AppBundle\Controller; use JMS\DiExtraBundle\Annotation as DI; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use JMS\SecurityExtraBundle\Annotation\Secure; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use AppBundle\Entity\Post; use AppBundle\Form\PostType; /** * @DI\Service("app.controller.post", scope="request") * @Route(service="app.controller.post") */ class PostController extends Controller { /** * @DI\Inject("service_container") */ public $container; /** * @DI\Inject("app.manager.post") * @var \AppBundle\Manager\PostManager */ public $postManager; /** * @Route("/add", name="post_add") * @Template * @Secure(roles="ROLE_USER") */ public function addAction() { $post = new Post(); $form = $this->createForm(new PostType(), $post); if ($this->getRequest()->getMethod() == 'POST') { $form->bind($this->getRequest()); if ($form->isValid()) { $this->postManager->addPost($post, $this->getUser()); return $this->redirect($this->generateUrl('post_list')); } } return array( 'form' => $form->createView() ); } } 




scope = "request" - this attribute is described in detail in the documentation.

@ Route (service = "app.controller.post") - informs the routing system that this controller is used as a service. In this case, the string values ​​of the redirection rules will change from 'AppBundle: Post: add' to 'app.controller.post:addAction'.

Using the @DI \ Inject dependency ("service_container") requires the parent controller class Symfony \ Bundle \ FrameworkBundle \ Controller \ Controller. As a controller, any classes are allowed, not necessarily derived from a standard controller - in this case, the dependence on DIC can be excluded.



Thus, we have created the app.controller.post service, dependent on service_container and app.manager.post :



Service access to service_container means that it has access to all public services of the project at once (through $ this-> container-> get ('...')). This makes it easier to use the framework, but it’s almost impossible to track communications between services with this approach. Therefore, for application services, it is recommended to use the public attribute = false and follow the rule:





Step 3. Creating a form



This step is not mandatory and rather serves to demonstrate the possibilities and consolidate the material. But in large projects that use a large number of forms, it can be useful to control connections.

In the created controller, we used the PostType form, we will try to define it as a service:

/src/AppBundle/Form/PostType.php
 <?php namespace AppBundle\Form; use JMS\DiExtraBundle\Annotation as DI; /** * @DI\Service("app.form.post", public=false) */ class PostType extends AbstractType { /* ... */ } 




Let's use the created service in the controller:

/src/AppBundle/Controller/PostController.php
 /* ... */ class PostController extends Controller { /** * @DI\Inject("app.form.post") * @var \AppBundle\Form\PostType */ public $postType; public function addAction() { $post = new Post(); $form = $this->createForm($this->postType, $post); /* ... */ } } 




Now we can trace the connection between the form and the controller:





Step 4. Creating a repository



First way: factory creation



Using Doctrine repositories as services is complicated by the fact that they are not part of Symfony, but are part of Doctrine. But such an opportunity is still there, through the factory creation of services. Unfortunately, at the moment it is not supported through annotations, so you will have to use configs.

In the Doctrine-entity file, specify the path to the repository:

/src/AppBundle/Entity/Post.php
 <?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository") */ class Post { /* ... */ } 




Create a PostRepository to get the page list of posts:

/src/AppBundle/Repository/PostRepository.php
 <?php namespace AppBundle\Repository; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\Pagination\Paginator; class PostRepository extends EntityRepository { public function getListPaginator($first, $max) { $qb = $this->createQueryBuilder('p') ->orderBy('p.id', 'DESC') ->setFirstResult($first) ->setMaxResults($max); return new Paginator($qb->getQuery()); } } 




We define the created class as the app.repository.post service:

/src/AppBundle/Resources/config/services.yml
 services: app.repository.post: class: AppBundle\Repository\PostRepository factory_service: doctrine.orm.entity_manager factory_method: getRepository public: false arguments: [AppBundle\Entity\Post] 




Add a repository and a list page to the controller:

/src/AppBundle/Controller/PostController.php
 /* ... */ class PostController extends Controller { /** * @DI\Inject("app.repository.post") * @var \AppBundle\Repository\PostRepository */ public $postRepository; protected $itemsPerPage = 10; /** * @Route("/list/{page}", requirements={"page"="\d+"}, defaults={"page"=1}, name="post_list") * @Template */ public function listAction($page) { $posts = $this->postRepository->getListPaginator( $first = ($page-1)*$this->itemsPerPage, $max = $this->itemsPerPage ); return array( 'posts' => $posts, 'page' => $page, 'pagesCount' => ceil(count($posts)/$this->itemsPerPage), ); } /* ... */ } 




The second way: the creation of repositories-wrappers



This method uses the Adapter pattern. The standard Doctrine repository is included in our own service repository. Unlike the first method, here everything is realizable by annotations.

In the Doctrine-entity file, we remove the repository directive:

src / AppBundle / Entity / Post.php
 <?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity() */ class Post { /* ... */ } 




We will need a parent class Repository, dependent on doctrine.orm.entity_manager and implementing the necessary functions of the repository. To do this, we use the inheritance of services :

/src/AppBundle/Repository/Repository.php
 <?php namespace AppBundle\Repository; use JMS\DiExtraBundle\Annotation as DI; /** * @DI\Service("app.repository", abstract=true) */ class Repository { /** * @DI\Inject("doctrine.orm.entity_manager") * @var \Doctrine\ORM\EntityManager */ public $em; protected $repositoryName; /** @return \Doctrine\ORM\EntityRepository */ protected function getDoctrineRepository() { return $this->em->getRepository($this->repositoryName); } public function find($id) { return $this->getDoctrineRepository()->find($id); } public function findOneBy(array $criteria) { return $this->getDoctrineRepository()->findOneBy($criteria); } public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) { return $this->getDoctrineRepository()->findBy($criteria, $orderBy, $limit, $offset); } public function findAll() { return $this->getDoctrineRepository()->findAll(); } /** @return \Doctrine\ORM\QueryBuilder */ public function createQueryBuilder($alias) { return $this->getDoctrineRepository()->createQueryBuilder($alias); } } 




The service definition will now be in the wrapper repository class itself:

/src/AppBundle/Repository/PostRepository.php
 <?php namespace AppBundle\Repository; use JMS\DiExtraBundle\Annotation as DI; use Doctrine\ORM\Tools\Pagination\Paginator; /** * @DI\Service("app.repository.post", parent="app.repository", public=false) */ class PostRepository extends Repository { protected $repositoryName = 'AppBundle:Post'; public function getListPaginator($first, $max) { $qb = $this->createQueryBuilder('p') ->orderBy('p.id', 'DESC') ->setFirstResult($first) ->setMaxResults($max); return new Paginator($qb->getQuery()); } } 




Note: when using annotations, specifying the parameter parent = "app.repository" is not required. JMSDiExtraBundle substitutes it automatically, based on the parent class.



Both methods implement the same functionality and are interchangeable. Therefore, the controller code will not change:

/src/AppBundle/Controller/PostController.php
 /* ... */ class PostController extends Controller { /** * @DI\Inject("app.repository.post") * @var \AppBundle\Repository\PostRepository */ public $postRepository; protected $itemsPerPage = 10; /** * @Route("/list/{page}", requirements={"page"="\d+"}, defaults={"page"=1}, name="post_list") * @Template */ public function listAction($page) { $posts = $this->postRepository->getListPaginator( $first = ($page-1)*$this->itemsPerPage, $max = $this->itemsPerPage ); return array( 'posts' => $posts, 'page' => $page, 'pagesCount' => ceil(count($posts)/$this->itemsPerPage), ); } /* ... */ } 




As a result, the dependency graph of the application will take the following form:



To control relationships with this approach, you must follow the rule:





Step 5. Optimization



As you have already noticed, all dependencies of the service are its variables and are created along with the creation of this service. In turn, when creating dependencies, their dependencies are created, thus all the elements of the subtree of dependencies, including unused ones, are created. By the example of app.controller.post, we see that the addAction function uses app.manager.post and app.form.post, and the listAction function is app.repository.post. But all the variables are created when creating the controller, therefore, no matter what function we call, some of the variables will be unused: in the case of addAction, this is app.repository.post, in the case of listAction, app.manager.post and app.form.post. This class consists of two independent parts, in which case they say that it has a low connectivity . The more methods a method works with a large number of variables, the higher the connectivity of this method with its class. The class in which each variable is used by each method has maximum connectivity. Creating classes with maximum connectivity is not always possible, but in our case this is easily achieved by dividing app.controller.post into two independent classes:





Conclusion



The introduction of dependencies in all classes of the system improves the quality of the architecture, but increases its complexity and development time. Therefore, this approach is justified only in large projects. When creating small projects like the site created in an article, you should give it up, however, as well as from using Symfony (in favor of lightweight frameworks).



The working source can be downloaded / viewed here:

github.com/cerritus/demoblog

Source: https://habr.com/ru/post/165237/



All Articles