Как упороться по DDD, модульной структуре и областям ответсвенности в Laravel. А потом стать счастливым =)
#Как упороться по DDD, модульной структуре и областям ответственности в Laravel. А потом стать счастливым.
[UPD] после пары вопросов в личку, решил добавить дисклеймер: Я не считаю, что это единственно верный путь. Я просто говорю вам о том, что существует такой подход.
Когда меня спрашивают для чего нужны сервис-провайдеры в Laravel, я пожимаю плечами и говорю: если вы не знаете зачем они нужны, значит они вам не нужны. Если вы пишите и строите код так, как это описано во всех мануалах, скорее всего вам хватит одного провайдера на всё приложение, и он уже есть сразу. И не надо парить мозг себе и людям. Просто забейте на это все.
Дефолтная структура приложения на laravel выглядит вот так: У вас есть папка Http
в которой лежат посредники(раньше это были фильтры) и контроллеры. Так же есть команды, хэндлеры, исключения, модели (последние Тейлор бессовестно бросил просто так - прямо в корне app )... возможно вы сами создаете папки репозиториев, обсерверов... или что-то там еще... потом вы начинаете строить приложение...
Вот вы усердно строчите свой код, прилежно создаете свои классы, аккуратно распихиваете их по папочкам. У вас получается большое приложение которое делает все что нужно, ну прям Code Happy по Daylee Rees. И вот в какой-то момент, вы решаете внедрить новую фичу взамен старой. И что же происходит? Вы как индейцы скачете по своим вьюхам выпиливая переменные, шерстите модели переназначая связи... и задерживаете дыхание в очередной раз обновляя страницу... ну вот - слава богу вы все выпили и перепили... а через неделю на какой-то далекой странице на которую никто не ходит, вдруг оказалось, что что-то не работает... вы просто забыли что там, еще что-то было... ну да и хрен с ней, все равно эта страница никому не нравилась. Или нет? Все верно - это ваш код. И он не работает. Вы послушно получаете пинка от заказчика/начальника и идёте чинить этот геморрой. Но ведь можно было всего этого избежать...
Давайте я покажу вам вот такую структуру приложения: Что, если я скажу вам, что я могу удалить любую из этих папок и мое приложение продолжит работать, как ни в чем не бывало? Вы не поверите? И правильно - еще мне нужно будет удалить провайдер из загрузки =) Как же это стало возможным? А возможным это стало благодаря двум крохотным пакетам: Widget-system и Tentacle Оба пакета работают как на laravel 4, так и на laravel 5. Однако все примеры будут приведены для laravel 5. Но обо всем по порядку. Начнем со структуры приложения...
Все мое приложение поделено на области ответственности. Что это такое? Это такие маленькие участки моего приложения, которые даже можно считать самостоятельными приложениями "вещами в себе" относительно друг друга. Например...
у меня есть область ответственности User. Она включает в себя модели User, Role, контроллеры управления, авторизации и регистраци, обсерверы, репозитории. В общем все как у полноценного приложения.
А так же, у меня есть область ответственности Menu, она включает в себя непосредственно Меnu и Items (пункты меню). Все сказанное об области User справедливо и для области Menu. Следите за пространствами имен классов, которые я буду приводить ниже в качестве прмеров, чтобы понять, где мы находимся. И так...
##Widget-system Давайте разберем классическую схему сайта. Как мы обычно решаем эту проблему? Чаще всего мы имеем некоторый шаблон, который предоставляет контент, и этот шаблон расширяет основной лэйаут. То есть в контроллере мы имеем что-то вроде:
return view('some');
а сам шаблон выглядит так:
@extends('layout')
@section('content')
<div>some content</div>
@stop
а уже в лэйауте:
@include('menu')
@include('left-side-bar')
@yield('content')
@include('right-side-bar')
Как же мы предоставляем переменные, которые должен получать лэйаут? Часто, это бывает что-то в духе View::share()
. Парни попродвинутее используют View::creator() или View::composer(), которые привязываются к соответствующим шаблонам.
В чем недостаток подобного подхода? В том, что вы жестко привязаны к этой структуре, и вам нужно модифицировать все это, когда вам нужно что-то добавить или убрать.
Как же эту проблему решает Widget-system? А вот так:
{!! Widget::show('menu') !!}
{!! Widget::position('left-side-bar') !!}
@yield('content')
{!! Widget::position('right-side-bar') !!}
Вне зависимости от того, определены ли виджеты, этот код уже работает, и не будет выдавать ошибок. А теперь заглянем в сервис провайдер области ответственности Menu:
<?php namespace App\Menu;
use Widget;
use App\Core\Providers\AbstractProvider;
#..
class Provider extends AbstractProvider{
#..
public function boot()
{
#..
Widget::register('App\Menu\Widgets\SimpleMenuWidget', 'menu');
#..
}
Это означает следующее: как только будет вызван виджет {!! Widget::show('menu') !!}
класс Widget найдет внутри себя соответствующий класс, создаст его объект и выполнит на нем метод render()
, результат исполнения этого метода вернется назад и будет выведен в шаблон. Пример класса-виджета:
<?php namespace App\Menu\Widgets;
use Illuminate\Contracts\Support\Renderable;
use App\Menu\Models\Item;
class SimpleMenuWidget implements Renderable {
public function render()
{
$items = Items::all();
return view('menu::menu.template', compact('items'));
}
}
Опустим детали того как именно отрисовывается менюшка в шаблоне 'menu.template' - это сейчас не важно. Вместо этого давайте представим, что нам нужно отрисовать менюшку с одними и теми же пунктами как в шапке, так и в футере, данные одинаковые, а шаблоны разные.
Немного изменим класс виджета
<?php namespace App\Menu\Widgets;
use Illuminate\Contracts\Support\Renderable;
use App\Menu\Models\Item;
class SimpleMenuWidget implements Renderable {
//мы установили шаблон по умолчанию
protected $defaultView = 'menu::menu.template';
//метод render() теперь принимает параметр
public function render($view = null)
{
// проверка - если $view определен,
// то он идет дальше. Иначе устанавливается дефолтный
$view = $view ? $view : $this->defaultView;
$items = Items::all();
return view($view, compact('items'));
}
}
Тогда в шаблоне мы можем применить такой ход:
{!! Widget::show('menu') !!}
#...
{!! Widget::show('menu', 'menu-bottom.template') !!}
Но вот же косяк... таким образом мы получили два запроса в базу, а ведь переменные одни и те же... Widget-system знает об этой проблеме. Нужно лишь переделать немного класс виджета.
<?php namespace App\Menu\Widgets;
use App\Menu\Models\Item;
class SimpleMenuWidget {
protected $defaultView = 'menu::menu.template';
protected $items;
//вынесли загрузку айтемов в конструктор
public function __construct()
{
$this->items = Items::all();
}
public function render($view = null)
{
$view = $view ? $view : $this->defaultView;
return view($view, ['items' => $this->items]);
}
}
дело в том, что Widget-system хранит объект виджета, и если он был однажды вызван, то повторный его вызов приведет к обращению к тому же объекту. Таким образом, конструктор вызывается лишь однажды, а render() вызывается каждый раз при обращении.
Само собой разумеется, вы можете передать любое количество дополнительных аргументов;
Widget::show('menu', $arg1, $arg2 , $argN)
Кроме того widget-system умеет работать с перегрузкой методов, например:
Widget::menu($arg1, $arg2 , $argN)
// тоже самое что
Widget::show('menu', $arg1, $arg2 , $argN)
Это, в сочетании c любым количеством передаваемых аргументов, открывает огромные возможности для фантазии и творчества =)
Но поговорим немного о другом... в первом примере шаблона я употребил метод Widget::position('left-side-bar')
. Что же это значит? Давайте, снова вернемся в сервис-провайдер области ответственности Menu и добавим туда еще кое-что.
public function boot()
{
#...
Widget::register('App\Menu\Widgets\SimpleMenuWidget', 'menu');
// вот это мы добавим
Widget::register('App\Menu\Widgets\LeftMenuWidget', 'left-menu', 'left-side-bar', 0);
}
Опустим детали и не будем вдаваться в то, как именно рисуется это "левое меню". Обратим лучше внимание на третий и четвертый аргументы. Третий аргумент - это имя позиции в которой будет отображен виджет, а четвертый - приоритет вывода.
Теперь пойдем в сервис-провайдер зоны ответственности Article
public function boot()
{
Widget::register('App\Article\Widgets\LastArticlesWidget', 'last-articles', 'left-side-bar', 1);
}
И снова опустим детали реализации, и посмотрим на суть: оба модуля опубликованы в одной позиции, с разным приоритетом вывода. Соответственно в левом сайдбаре первым будет отображен модуль "левого меню", и сразу за ним модуль "последние статьи". Таким же образом мы можем назначать сколько угодно позиций. Это и позволит отделиться от модулей областей ответственности на столько, насколько это вообще возможно.
Стоит так же отметить, что все классы-виджеты вызываются через App::make(); А это значит, что зависимости которые вы укажете в методе-конструкторе виджета будут по возможности разрешены.
Вот поэтому этот крохотный класс widget-system так крут. Надеюсь вам он тоже понравится.
##Tentacles Окей, а как же связи моделей? - спросите вы. Тут к нам на помощь придет другой малютка: класс-трейт - Тентакль.
Области ответственности это хорошо, но как же быть с их пересечениями? Во все свои модели, я подмешиваю трейт Tentacle
. Он содержит несколько методов, которые отвечают за перегрузку отсутствующих методов связи на заранее подгруженные. Сейчас я расскажу как это работает.
Как мы помним, у нас есть область ответственности Article, и само собой, что у статьи, к примеру, должен быть автор. Но было бы очень странно, если бы у модели User сразу же была связь articles
, ведь это совершенно другая область ответственности. Но моя модель User имеет трейт Tentacle
- и это прекрасно. Теперь я иду в сервис-провайдер области ответственности Article и добавляю в метод boot()
следующий код:
User::addRelation('articles', function(Model $user){
$user->hasMany(Article::class);
})
И теперь наш класс User может использоваться так:
$user = User::with('articles')->get();
А теперь обратите внимание, что мы ни разу не вторглись в область ответственности User, но при этом привязали к нему статьи. Мы не вторглись в область ответственности Frontend, которая хранит лэйауты для фронта и отвечает за отображение главной страницы. Тем не менее, главная страница упакована необходимыми меню и модулями.
Для того, чтобы начать так работать нужно очень чутко ощущать эти самые области ответственности, и очень тонко понимать что и зачем делается, и что к чему относится. Это совсем не просто. Возьмем простой пример, профиль пользователя. И в нем закладки
казалось бы все просто - в области ответственности User есть UserController
, а в нем метод profile
. Выбираем пользователей вместе с его статьями, новостями, черновиками... ай! Не бей по рукам! Ну хорошо... Так делать по DDD нельзя. Мы только что вторглись в чужую область ответственности. Вместо этого нужно обозначить в шаблоне:
{!! Widget::position('user-matherials', $user) !!}
и в соответствующих сервис-провайдерах зарегистрировать виджеты для этой позиции. Во все виджеты будет передан объект юзера, с него загрузятся необходимые связи и из связей отрисуются вкладки. Теперь даже если удалим область ответственности News или Article, наш сайт продолжит работу, и все с ним будет хорошо. Так что, я повторюсь... все эти вещи нужно чувствовать очень тонко.
Общение между областями ответственности должно происходить посредством событий и их слушателей. Реже - адаптеров.
Конечно же это еще не DDD. Это лишь одно из условий его обеспечивающих - модульность (слабая связанноть). Сам по себе DDD намного больше, и это скорее философия, чем реальный паттерн. Тем не менее, я надеюсь, что хоть немного пролил свет на модульный подход. Удачи!
P.S. Если вам вдруг тоже приспичит упороться, пользуйтесь этими пакетами аккуратно, они еще сыроваты и скорее всего будут дорабатываться. Доки по пакетам пока что убогие) но основные их возможности уже были изложены в данной статье.
P.P.S. Отдельное спасибо @sleeping-owl. Ну и спасибо всему Cooбществу. Пишите в чатике, курите мануалы