Skip to content

Instantly share code, notes, and snippets.

@greabock
Last active December 13, 2023 14:49
Show Gist options
  • Save greabock/48787baab768b519f21c to your computer and use it in GitHub Desktop.
Save greabock/48787baab768b519f21c to your computer and use it in GitHub Desktop.
Как упороться по модульной структуре и областям ответсвенности в Laravel. А потом стать счастливым =)

#Как упороться по модульной структуре и областям ответственности в 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. Выбираем пользователей вместе с его статьями, новостями, черновиками... ай! Не бей по рукам! Ну хорошо... Так делать нельзя. Мы только что вторглись в чужую область ответственности. Вместо этого нужно обозначить в шаблоне:

{!! Widget::position('user-matherials', $user) !!}

и в соответствующих сервис-провайдерах зарегистрировать виджеты для этой позиции. Во все виджеты будет передан объект юзера, с него загрузятся необходимые связи и из связей отрисуются вкладки. Теперь даже если удалим область ответственности News или Article, наш сайт продолжит работу, и все с ним будет хорошо. Так что, я повторюсь... все эти вещи нужно чувствовать очень тонко.

Общение между областями ответственности должно происходить посредством событий и их слушателей. Реже - адаптеров.

Я надеюсь, что хоть немного пролил свет на модульный подход. Удачи!


P.S. Если вам вдруг тоже приспичит упороться, пользуйтесь этими пакетами аккуратно, они еще сыроваты и скорее всего будут дорабатываться. Доки по пакетам пока что убогие) но основные их возможности уже были изложены в данной статье.

P.P.S. Отдельное спасибо @sleeping-owl. Ну и спасибо всему Cooбществу. Пишите в чатике, курите мануалы

@nikorgl
Copy link

nikorgl commented Sep 15, 2016

А есть какой-нибудь бойлерплейт такой модульной реализации Laravel?

@yazux
Copy link

yazux commented Oct 17, 2016

Очень полезная статья, спасибо. Искал пример именно такого подхода.

@bigperson
Copy link

https://github.com/arrilot/laravel-widgets а этот пакет для системы виджетов не пойдет?

@grscl
Copy link

grscl commented Mar 16, 2017

Тентакль бестолковая прослойка, для связей можно было обойтись трейтом без сервис провайдера ;)

@Piterden
Copy link

Посмотрите на PyroCMS. Это - альтернативная реализация подхода DDD. Я б даже сказал, это NoDDD )))

@shov
Copy link

shov commented Feb 16, 2018

Огонь! Пока не дочитал до тентакля)

На самом деле проблемы будут если удалить допустим директорию User,
хотябы в Article есть в провайдере вот это

User::addRelation('articles', function(Model $user){
  $user->hasMany(Article::class);
})

и это уже все развалит, зависимость от класса User и его присутствия в системе в том namespace откуда он use очевидна

Было бы куда веселее что-тов роде

$tentakle->tryAddRelation('App\\User\\Models\\User', 'articles', function( ....

и если такой модели нет, то.. просто ничего не делать

@4n70w4
Copy link

4n70w4 commented Jan 14, 2020

На данный момент Tentacle не совместим с Laravel 6. Есть какие-либо альтернативы?

@greabock
Copy link
Author

@4n70w4 создать пулл-реквест )
можно еще в сторону Macroable посмотреть

@4n70w4
Copy link

4n70w4 commented Jan 29, 2020

Посмотрел в сторону Macroable, требовалось внести фикс, чтобы он работал в том числе и с моделями.
laravel/framework#31286

@salimonenko
Copy link

"в базе останется мусор. Но он абсолютно не помешает работе приложения"
Ну, всё, дальше можно не обсуждать. Лично я буду делать чтобы как уж нибудь без мусора. Раздражает, знаете ли, мусор.

Если откровенно, мой скромный и исключительно субъективный опыт использования Laravel говорит следующее: сие годится, разве что, чтобы по-быстрому сляпать что-то типа "работающего сайта" и впарить заказчику. В итоге, ежели возникнут проблемы (в том числе. оттого, что разработчик фреймворка выпустит очередную несовместимую версию), потом программист потратит ГОРАЗДО больше времени на выявление тех самых таинств модульной структуры и их согласование, чтобы, мол, "заработало".
Нет уж. Лично для себя и для тех, кого не хочу подводить - буду делать так, как делал раньше: вручную. И кроссбраузерно.
А вот всем остальным - по желанию.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment