#Как упороться по модульной структуре и областям ответственности в Laravel. А потом стать счастливым.
[UPD] после пары вопросов в личку, решил добавить дисклеймер: Я не считаю, что это единственно верный путь. Я просто говорю вам о том, что существует такой подход.
Когда меня спрашивают для чего нужны сервис-провайдеры в Laravel, я пожимаю плечами и говорю: если вы не знаете зачем они нужны, значит они вам не нужны. Если вы пишите и строите код так, как это описано во всех мануалах, скорее всего вам хватит одного провайдера на всё приложение, и он уже есть сразу. И не надо парить мозг себе и людям. Просто забейте на это все.
Дефолтная структура приложения на laravel выглядит вот так: У вас есть папка Http в которой лежат посредники(раньше это были фильтры) и контроллеры. Так же есть команды, хэндлеры, исключения, модели (последние Тейлор бессовестно бросил просто так - прямо в корне app )... возможно вы сами создаете папки репозиториев, обсерверов... или что-то там еще... потом вы начинаете строить приложение...
Вот вы усердно строчите свой код, прилежно создаете свои классы, аккуратно распихиваете их по папочкам. У вас получается большое приложение которое делает все что нужно, ну прям Code Happy по Daylee Rees. И вот в какой-то момент, вы решаете внедрить новую фичу взамен старой. И что же происходит? Вы как индейцы скачете по своим вьюхам выпиливая переменные, шерстите модели переназначая связи... и задерживаете дыхание в очередной раз обновляя страницу... ну вот - слава богу вы все выпили и перепили... а через неделю на какой-то далекой странице на которую никто не ходит, вдруг оказалось, что что-то не работает... вы просто забыли что там, еще что-то было... ну да и хрен с ней, все равно эта страница никому не нравилась. Или нет? Все верно - это ваш код. И он не работает. Вы послушно получаете пинка от заказчика/начальника и идёте чинить этот геморрой. Но ведь можно было всего этого избежать...
Давайте я покажу вам вот такую структуру приложения:

Все мое приложение поделено на области ответственности. Что это такое? Это такие маленькие участки моего приложения, которые даже можно считать самостоятельными приложениями "вещами в себе" относительно друг друга. Например...
у меня есть область ответственности 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. Выбираем пользователей вместе с его статьями, новостями, черновиками... ай! Не бей по рукам! Ну хорошо... Так делать нельзя. Мы только что вторглись в чужую область ответственности. Вместо этого нужно обозначить в шаблоне:
{!! Widget::position('user-matherials', $user) !!}и в соответствующих сервис-провайдерах зарегистрировать виджеты для этой позиции. Во все виджеты будет передан объект юзера, с него загрузятся необходимые связи и из связей отрисуются вкладки. Теперь даже если удалим область ответственности News или Article, наш сайт продолжит работу, и все с ним будет хорошо. Так что, я повторюсь... все эти вещи нужно чувствовать очень тонко.
Общение между областями ответственности должно происходить посредством событий и их слушателей. Реже - адаптеров.
Я надеюсь, что хоть немного пролил свет на модульный подход. Удачи!
P.S. Если вам вдруг тоже приспичит упороться, пользуйтесь этими пакетами аккуратно, они еще сыроваты и скорее всего будут дорабатываться. Доки по пакетам пока что убогие) но основные их возможности уже были изложены в данной статье.
P.P.S. Отдельное спасибо @sleeping-owl. Ну и спасибо всему Cooбществу. Пишите в чатике, курите мануалы

