Сервисы в Rails, Symfony, Yii2 и Zend Expressive: реализуем одну задачу на четырех фреймворках
Долгое время муссировалась и продолжает муссироваться в кругах разных каркасов идея сервисов. Во многих фреймворках сервисы стали самостоятельными единицами кода (как скажем, в Symfony о чем мы еще поговорим). В Rails же паттерн Service Object выглядит очень просто, так как никаких «самостоятельных единиц» именуемых сервисами, в нем нет.
В этой статье я покажу сначала разработку страницы, использующей сервис в Symfony 3.3.9 а затем — то же самое в Rails. Сразу скажу читатели раздела Rails будут слегка разочарованы — «он опять одну строчку написал». Ну извините, это же пример.
Несмотря на то что я являюсь ROR разработчиком, каркас Symfony мне очень понравился. Я считаю его документацию и продукты такие как twig — лучшими в нише. Надеюсь, с выходом версии 4 такая тенденция не утеряется. А выход этот уже произошел.
Итак, что будет за задача?
Почешем там где у нас чешется — есть у меня просто папка в локальной файловой системе, там находится куча шахматных книжек. Я хочу сделать страницу где бы они перечислялись.
Для этого мы и используем сервисы — в нашем случае сервис какой возвращает список книг (файлов) в удобном для нас виде. Минуя директории.
Делаем это на Symfony
Программисты на чистом PHP сразу же бросятся делать readdir и много чего другого в стиле «да я щас за пять минут». Мы тоже не будем усложнять, но использовать мы будем нормальный подход — генератор DirectoryIterator.
Хотя, как уже не единожды заметили симфонисты, лучшей практикой именно для Symfony будет использование специального компонента для нахождения файлов - Finder
Итак, у нас есть Symfony 3.3.9. Создадим-ка контроллер книг, какой собственно будет запускаться для отображения страницы
Как видим, контроллер у нас простой. Он обращается к сервису, какой внедряется к нему в качестве зависимости с помощью DI, получает список файлов. А потом просто отображает шаблон, передав туда этот список.
Обратите внимание, что класс контроллера наследован от AbstractController. Это налагает ограничение на вызовы сервисов из сервис-контейнера внутри экшена, и делает разработку более строгой. Теперь, наследовав так, мы можем только совершать инжект зависимости (при помощи type-hint) но не вызывать эту зависимость из сервис-контейнера внутри экшена.
Эти действия предназначены для более строгой разработки, чтобы не было случаев когда у нас есть экшн какой делает 10 дел и распухает в «метод-Бог» а мы этого или не видим, или не желаем видеть. В случае же инжектов сразу ясно — если у вас параметры метода содержат 20 инжектов значит происходит что-то очень нехорошее и вам нужно срочно рефакторить приложение.
Займемся сочинением непосредственно нашего сервиса:
В случае же использования Finder, исполняемый код метода выглядел бы примерно так (подсказал Кирилл Несмеянов):
Сервис наш должен быть многоцелевым, применяться неоднократно, повторно использоваться. По этому в него с помощью аргумента конструктора внедряется параметр path какой указывает ИЗ КАКОЙ директории собственно, возвращать список файлов. В Symfony это называется Autowiring. Так же отметьте, что мы используем генератор DirectoryIterator, а не наколенный перебор файлов в директории. Это дает нам удобные методы isDir и другие.
Куда же теперь поместить параметр пути к файлам? Симфони дает однозначный ответ, это файл
Он по умолчанию не ставится на контроль версий git, и вообще предназначен для «переносных» меняющихся настроек. Вот туда-то мы и положим значение для сервиса, какое он автоматически прочитает:
Но это еще не все. В файле
нам необходимо указать, что наш сервис принимает этот параметр:
Отлично! Все почти готово. Если сейчас обратиться по адресу /app_dev.php/books/list вашего хоста, выскочит ошибка о том что не найден шаблон twig. Поправим это:
Все теперь работает. Полюбуемся на результат, зайдя на хост (у меня это symfony.local) то есть мой адрес будет http://symfony.local/app_dev.php/books/list а продакшн http://symfony.local/books/list. Не забудьте перед запуском продакшна сделать регенерацию кеша симфони (в том числе он кеширует роуты):
Сделав все эти манипуляции с кешем, или зайдя как на скрине — через app_dev.php то есть с поддержкой дебаг тулбара, мы увидим следующую милую картину: (посмотреть увеличенную):
Делаем это на Ruby on Rails
Уфф, тут кода будет меньше, и телодвижений тоже. Но это не значит что Symfony чем-то хуже или что он громоздок. Концепция сервисов как практикуемых единиц очень полезна и хороша. В Rails же мы начнем с простого. Сначала создадим само приложение:
А теперь создадим-ка контроллер, для простоты с всего одним действием, как и в Symfony:
Чтобы использовать паттерн Service Object, по сути в Rails достаточно положить файл представляющий «сервис» куда-нибудь в такое место, где он будет автоматически загружен. Создадим файл-сервис в app/services, по умолчанию этой папки нет но сейчас появится:
Теперь у нас есть файл, какой будет загружен Rails самостоятельно. Заполним его:
Вот и весь сервис. За счет гибкости Ruby мы получили гораздо меньше кода для перебора директории.
Используем его в контроллере:
И наконец, заполним вид (view):
Вот и все! Теперь мы увидим мало отличающуюся от Symfony-реализации картину, запустив
и перейдя на http://localhost:3000/books/index :
Тоже можно увеличить.
Теперь поговорим о гибкости решения. Во-первых поскольку мы в сервисе используем Dir.glob то мы ставим звездочку в конце нашего пути к файлам. Во-вторых этот самый путь мы прописываем прямо там, где сервис используется — в контроллере. Это не совсем гибко! Но идеология Rails состоит как раз в том чтобы избавляться от внешних файлов настроек, даже таких безобидных как parameters.yml в Symfony. Да, мы конечно можем все-таки оформить путь к файлам как статичную настройку. Но делать это по крайней мере, в данной статье мы не будем. Не потому что «и так сойдет» а потому что это Rails. И не факт что другой контроллер запросит уже иной путь, тогда наше решение выиграет по гибкости так как путь можно указывать создавая класс объекта-сервиса.
Подведя итоги, скажу что оба каркаса хоть я и за ROR, прекрасно справляются с задачей, просто каждый своим путем. В Symfony можно тоже сделать сервис гибким, чтобы передавать в него параметр при вызове каждый раз но это я оставил «за кулисами» статьи, предпочтя автосвязывание аргументов из файла настроек.
Делаем это на Yii2
Так как я общаюсь и уважаю многих разработчиков на фреймворке Yii2, камрад lavros любезно написал гайд как сделать задачу на yii2. И это оказалось очень интересным, так как он показал сильные стороны фреймворка, например генерация кода консольным gii но об этом ниже. Вот этот гайд, передаю слово автору.
Сделаем тоже самое на Yii2. Создадим контроллер, действие и представление. Реализуем сервис по получению списка файлов из указанной директории. Внедрим сервис-объект через конструктор в контроллер, получим данные в действии контроллера и отразим в представлении.
Итак, поехали!
В качестве шаблона приложения воспользуемся yii2-app-basic.
Развернём шаблон yii2-app-basic с помощью composer:
Приложение развернули. Создадим контроллер, действие и представление для будущей страницы. В Yii2 есть инструмент для генерации кода — Gii. Имеется web версия и консольная, мы воспользуемся консольной.
Для этого перейдём в каталог с приложением и выполним команду:
Результатом работы команды будет:
Страницу создали. Запустим приложение чтобы посмотреть на страницу:
Откроем браузер по адресу: http://localhost:8080?r=book/list, на странице увидим:
Хорошо, страница есть. Теперь реализуем сервис-объект для получения списка файлов. Создадим каталог для сервисов — services, в нём файл DirectoryListerService.php:
Реализуем метод getFileList() для получения списка файлов. В Yii2 есть помощник по работе с файлами — FileHelper. Воспользуемся FileHelper для получения файлов в указанной директории.
Отредактируем файл DirectoryListerService.php:
Сервис-объект реализован, подключим его в нашем контроллере controllers/BookController.php. Можно подключить явно, создав в методе actionList() экземпляр класса app\services\DirectoryListerService, но мы пойдём другим путём — воспользуемся внедрением зависимости через конструктор:
Для начала настроим контейнер зависимостей в конфигурации приложения, отредактируем config/web.php:
Объект описали как singleton, то есть, где бы мы не внедряли наш сервис-объект, экземляр будет создаваться один раз.
Внедрим в конструкторе contorllers\BookController.php:
Теперь при создании контроллера сервис DirectoryListerService будет создаваться автоматически.
Отредактируем метод BookController::actionList(), получим список файлов, посчитаем количество найденых файлов и передадим в представление:
Оформим представление действия views/book/list.php:
Готово. Обновляем страницу в браузере, смотрим результат.
Он будет таким же, как и в предыдущих примерах, только снабжен yii2 debug toolbar. Похожее мы видели в скриншоте для symfony — там тоже оставлен был тулбар для отладки.
В целом, мне очень понравился подход yii2 и это было действительно не сложно и не долго, при условии что это делает знакомый с документацией и возможностями yii2 человек.
Спасибо lavros за важный гайд! Уверен, некоторых это даже подстегнет изучать Yii2.
Список книг на Zend Expressive
Камрад Ми}{алы4 любезно продолжил нашу традицию и во мгновение ока написал гайд как сделать это же на Zend Expressive. Передаю слово ему.
Для решения задачи по отображению списка книг из директории используется Zend Expressive с модульной структорой, Zend ServiceManager, FastRoute, Twig, Whoops. Все эти пакеты можно выбрать на этапе создания нового проекта, когда вы выполняете команду
Создадим сервис в модуле App (модуль App создается по умолчанию после создания пустого проекта Zend Expressive):
Зарегистрируем наш сервис:
Создадим действие BookAction
в модуле App:
Зарегистрируем наше действие:
Зарегистрируем маршрут для действия:
Выполним переход по нашему только что добавленному маршруту http://localhost:8080/books :
(посмотреть увеличенную картинку):
Готово!
P.S. Процесс можно ускороить используя Command-Line Tool.