Для упрощения статьи сократим число страниц сайта до двух:
- Добавление нового поста (/add)
- Отображение списка всех постов (/list)
Финальная архитектура приложения будет выглядеть следующим образом:
Шаг 1. Создание произвольного сервиса
DI как часть Symfony уже рассматривался на хабре, а также подробно описан в документации. Поэтому мы сразу приступим к созданию собственных сервисов и зависимостей. Это можно сделать тремя способами: задание зависимостей в коде бандла, через конфигурационные файлы (YAML, XML, PHP) и используя аннотации (при помощи бандла JMSDiExtraBundle, входящего в стандартную комплектацию Symfony). Каждый способ имеет свои плюсы и минусы. Мы будем использовать аннотации для наглядности и сокращения объема кода. Начнем с класса, реализующего бизнесс-логику. Пусть это будет PostManager, обрабатывающий добавление нового поста:
<?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 — превращает класс в сервис. В параметрах аннотации указывается название сервиса (app.manager.post) и его атрибуты.
public=false — данный атрибут указывает на то, что созданный сервис нельзя будет вызывать напрямую из DIC ($container->get(‘app.manager.post’) приведет к ошибке). Созданный сервис смогут использовать только сервисы, зависящие от него явно (далее, на примере с контроллером, станет понятнее).
@DI\Inject — указание сервисов, от которых зависит созданный сервис. Использование данной аннотации возможно только с переменными типа public. Для private/protected переменных-зависимостей можно использовать @DI\InjectParams для конструктора или другие способы создания сервисов.
Итак, мы создали сервис app.manager.post, зависящий от doctrine.orm.entity_manager:
Графическое отображение сервисов и связей доступно в удобном веб-интерфейсе с установкой JMSDebuggingBundle.
Шаг 2. Создание контроллера
По умолчанию в Symfony контроллеры не являются сервисами, но в документации есть заметка, позволяющая их сделать таковыми. Создадим PostController, использующий ранее созданный PostManager:
<?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» — данный атрибут подробно описан в документации
@Rоute(service=«app.controller.post») — сообщает системе роутинга, что данный контроллер используется как сервис. При этом строковые значения правил переадресации изменятся с ‘AppBundle:Post:add’ на ‘app.controller.post:addAction’.
Использование зависимости @DI\Inject(«service_container») требует родительский класс-контроллер Symfony\Bundle\FrameworkBundle\Controller\Controller. В качестве контроллеров допускаются любые классы, не обязательно производные от стандартного контроллера — в этом случае зависимость от DIC можно исключить.
Таким образом, мы создали сервис app.controller.post, зависящий от service_container и app.manager.post:
Доступ сервиса к service_container означает, что он имеет доступ сразу ко всем public-сервисам проекта (через $this->container->get(‘…’)). Это облегчает использование фреймворка, но отследить связи между сервисами при таком подходе практически невозможно. Поэтому для сервисов приложения рекомендуется использовать атрибут public=false и следовать правилу:
Шаг 3. Создание формы
Данный шаг не является обязательным и служит скорее для демонстрации возможностей и закрепления материала. Но в объемных проектах, использующих большое количество форм, может быть полезным для контроля связей.
В созданном контроллере мы использовали форму PostType, попробуем определить её как сервис:
<?php namespace AppBundle\Form; use JMS\DiExtraBundle\Annotation as DI; /** * @DI\Service("app.form.post", public=false) */ class PostType extends AbstractType { /* ... */ }
Воспользуемся созданным сервисом в контроллере:
/* ... */ 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); /* ... */ } }
Теперь мы можем проследить связь формы и контроллера:
Шаг 4. Создание репозитория
Первый способ: фабричное создание
Использование Doctrine-репозиториев как сервисов осложняется тем, что они не являются частью Symfony, а входят в состав Doctrine. Но такая возможность всё же есть, через фабричное создание сервисов. К сожалению, на данный момент она не поддерживается через аннотации, поэтому придется использовать конфиги.
В файле Doctrine-сущности укажем путь к репозиторию:
<?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository") */ class Post { /* ... */ }
Создадим PostRepository для получения постраничного списка постов:
<?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()); } }
Определяем созданный класс как сервис app.repository.post:
services: app.repository.post: class: AppBundle\Repository\PostRepository factory_service: doctrine.orm.entity_manager factory_method: getRepository public: false arguments: [AppBundle\Entity\Post]
Добавим репозиторий и страницу со списком в контроллер:
/* ... */ 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), ); } /* ... */ }
Второй способ: создание репозиториев-обёрток
Данный способ использует паттерн Adapter. Стандартный Doctrine-репозиторий включается в наш собственный сервис-репозиторий. В отличие от первого способа, здесь все реализуемо аннотациями.
В файле Doctrine-сущности убираем указание репозитория:
<?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity() */ class Post { /* ... */ }
Нам понадобится родительский класс Repository, зависимый от doctrine.orm.entity_manager и реализующий необходимые функции репозитория. Для этого воспользуемся наследованием сервисов:
<?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); } }
Определение сервиса теперь будет в самом классе репозитория-обёртки:
<?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()); } }
Примечание: при использовании аннотаций, указание параметра parent=«app.repository» не является обязательным. JMSDiExtraBundle подставляет его автоматически, на основании родительского класса.
Оба способа реализуют одинаковый функционал и являются взаимозаменяемыми. Поэтому код контроллера не изменится:
/* ... */ 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), ); } /* ... */ }
В результате граф зависимостей приложения примет следующий вид:
Для контроля связей при таком подходе необходимо следовать правилу:
Шаг 5. Оптимизация
Как Вы уже заметили, все зависимости сервиса являются его переменными и создаются вместе с созданием этого сервиса. В свою очередь, при создании зависимостей, создаются их зависимости, таким образом создаются все элементы поддерева зависимостей, в том числе неиспользуемые. На примере app.controller.post мы видим, что функция addAction использует app.manager.post и app.form.post, а listAction – app.repository.post. Но все переменные создаются при создании контроллера, поэтому, какую бы мы функцию не вызвали, часть переменных обязательно будут неиспользуемыми: в случае addAction это app.repository.post, в случае listAction — app.manager.post и app.form.post. Данный класс как бы состоит из двух независимых частей, в таком случае говорят, что он обладает низкой связностью. Чем с большим количеством переменных работает метод, тем выше связность этого метода со своим классом. Класс, в котором каждая переменная используется каждым методом, обладает максимальной связностью. Создание классов с максимальной связностью не всегда является возможным, но в нашем случае этого легко добиться, разделив app.controller.post на два независимых класса:
Заключение
Внедрение зависимостей во все классы системы повышает качество архитектуры, но увеличивает её сложность и время разработки. Поэтому данный подход оправдывает себя только в больших проектах. При создании маленьких проектов, подобных созданному в статье сайту, от него стоит отказаться, впрочем, как и от использования Symfony (в пользу легковесных фреймворков).
Рабочие исходники можно скачать/посмотреть здесь:
github.com/cerritus/demoblog
ссылка на оригинал статьи http://habrahabr.ru/post/165237/
Добавить комментарий