Может войдёшь?
Черновики Написать статью Профиль

Хорошие практики Laravel: принцип единственной ответственности (Single Responsibility Principle)

Best practices Хорошие практики

Небольшое вступление

В мире Laravel существует очень серьезная, на мой взгляд, проблема. Laracasts, книги, видео туториалы, статьи и даже документация показывают нам использование плохих практик. Понятно, что делается это для популяризации фреймворка, снижая порог вхождения для новичков. Действительно, благодаря такому подходу, человек может написать работающее веб приложение при минимальных усилиях. И это хорошо. Плохо то, что разработчик продолжает писать низкокачественный код даже в сложных приложениях, в результате чего они порой становятся абсолютно неподдерживаемыми. Это значит, что любое изменение функционала в таком приложении занимает в разы, а иногда и в десятки раз больше времени разработчика и, соответственно, денег клиента. Также, эти правки образуют новые баги, ломают другие части приложения и т.д.

Проанализировав десятки реальных коммерческих приложений, написанных на Laravel, я понял, что практически все разработчики игнорируют даже самые простые хорошие практики написания кода.

Отталкиваться я буду не от заезженных терминов вроде DRY, SOLID, описания паттернов и пр. Подобных статей довольно много и их авторы, зачастую не понимая сути подобных принципов, вновь и вновь пересказывают их. А примеры кода в этих статьях зачастую не имеют ничего общего с кодом приложений из реального мира. Моя же цель - дать новичкам действительно полезный материал, с помощью которого они бы могли заметно улучшить качество своих приложений.

Принцип единственной ответственности (Single responsibility principle или SRP)

Хотите писать более качественный и поддерживаемый код, чем пишет 95% Laravel разработчиков? Изучите, впитайте и везде применяйте принцип единственной ответственности (далее SRP). Вы также можете прочитать о других хороших практиках Laravel, а также о хороших практиках разработки в контексте PHP.

Сейчас же мы возьмем кусочек кода контроллера, подобный которому вы можете увидеть практически во всех реальных Laravel приложениях. Затем, шаг за шагом, мы будем переписывать наш код, следуя при этом SRP. Итак, код, с которым мы будем работать:

public function store(Request $request)
{
    $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        'make_id' => 'required'
    ]);

    if (auth()->guest() || !auth()->user()->hasRole('moderator') || !auth()->user()->canAddContent()) {
        return redirect('/cars/models')->with('error', 'У вас нет прав для добавления новой модели');
    }

    $make = Make::find($request->make_id);

    if (!$make) {
        return redirect('/cars/models')->with('error', 'Неверная марка');
    }

    $model = $make->model()->create($request->all());

    if ($request->hasFile('image') && config('app.uploading_enabled')) {
        Image::make($request->file('image'))
             ->resize(300, null, function ($constraint) {
                 $constraint->aspectRatio();
             })
             ->save($public_path('images/models') . DIRECTORY_SEPARATOR . $model->id . '.jpg');
        }
    }

    return redirect('/cars/models')->with('message', 'Модель успешно добавлена');
}

Этот код добавляет новую модель автомобиля (model), привязывает его к марке автомобиля (make), обрабатывает загруженное изображение и выполняет несколько других функций.

Но SRP говорит нам о том, что этот метод контроллера должен выполнять лишь одну функцию. Контроллер должен лишь обработать запрос и отправить ответ. В результате нарушения этого важного принципа, контроллеры раздуваются до невероятных размеров и вносить изменения в существующий код, не сломав его, становится все сложнее и сложнее. Тестировать такой код также намного труднее, а иногда и вовсе невозможно. Давайте также вспомним о концепции тонких контроллеров и начнем рефакторинг.

1

Контроллер не должен заниматься валидацией, поэтому первое, что мы сделаем - создадим Request-класс и перенесем валидацию в него. Также обратите внимание на этот кусок кода:

$make = $this->make->find($request->make_id);

if (!$make) {
    return redirect('/cars/models')->with('error', 'Неверная марка');
}

Его мы заменим правилом валидации exists:makes,id. В результате этого, метод rules нашего ModelRequest-класса будет выглядеть так:

class ModelRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|max:255',
            'content' => 'required',
            'make_id' => 'required|exists:makes,id'
        ];
    }
}

Внедрим ModelRequest-класс в наш контроллер, который уже стал заметно тоньше:

public function store(ModelRequest $request)
{
    if (auth()->guest() || !auth()->user()->hasRole('moderator') || auth()->user()->canAddContent()) {
        return redirect('/cars/models')->with('error', 'У вас нет прав для добавления новой модели');
    }

    $model = $this->model->create($request->all());

    if ($request->hasFile('image') && config('app.uploading_enabled')) {
        Image::make($request->file('image'))
            ->resize(300, null, function ($constraint) {
                $constraint->aspectRatio();
            })
            ->save($public_path('images/models') . DIRECTORY_SEPARATOR . $model->id . '.jpg');
        }
    }

    return redirect('/cars/models')->with('message', 'Модель успешно добавлена');
}

2

Следующим шагом будет вынос бизнес логики в сервис-класс. В данном случае, мы вынесем работу с изображением:

class ModelService
{
    public function handleUploadedImage(Request $request, $modelId)
    {
        if ($request->hasFile('image') && config('app.uploading_enabled')) {
            Image::make($request->file('image'))
                 ->resize(300, null, function ($constraint) {
                     $constraint->aspectRatio();
                 })
                 ->save($public_path('images/models') . DIRECTORY_SEPARATOR . $model->id . '.jpg');
        }
    }
}

Используя контейнер Laravel, мы внедрим и будем использовать сервис-класс в контроллере, после чего последний будет выглядеть так:

protected $model;

protected $modelService;

public function __construct(Model $model, ModelService $modelService)
{
    $this->model = $model;

    $this->modelService = $modelService;
}

public function store(ModelRequest $request)
{
    if (auth()->guest() || !auth()->user()->hasRole('moderator') || auth()->user()->canAddContent()) {
        return redirect('/cars/models')->with('error', 'У вас нет прав для добавления новой модели');
    }

    $model = $this->model->create($request->all());

    $this->modelService->handleUploadedImage($model->id); // Обратите внимание на эту строку.

    return redirect('/cars/models')->with('message', 'Модель успешно добавлена');
}

3

Метод handleUploadedImage также выполняет слишком много задач. Следуя SRP, нам необходимо вынести часть логики в отдельные методы сервис-класса:

class modelService
{
    public function handleUploadedImage($modelId)
    {
        if ($this->canHandleImage()) {
            Image::make(request()->file('image'))
                 ->resize(300, null, function ($constraint) {
                     $constraint->aspectRatio();
                 })
                 ->save($this->getImagePath($modelId));
        }
    }

    protected function canHandleImage()
    {
        return request()->hasFile('image') && config('app.uploading_enabled');
    }

    protected function getImagePath($modelId)
    {
        return public_path('images/models') . DIRECTORY_SEPARATOR . $modelId . '.jpg';
    }
}

4

Должен ли контроллер делать проверку прав пользователей? Конечно же нет. Поэтому перенесем эту логику туда, где ей самое место: в политиках.

class ModelPolicy
{
    public function create(User $user)
    {
        return isModerator() && auth()->user()->canAddContent();
    }
}

5

В оригинальном коде мы имели два сообщения об ошибках и одно сообщение об успешном выполнении метода. После рефакторинга о сообщениях об ошибках будет заботиться уже Request-класс. Контроллер "не должен знать" о конкретном тексте, поэтому давайте также вынесем последнее сообщение в языковой файл. Теперь наш контроллер выглядит так:

public function store(ModelRequest $request)
{
    $model = $this->model->create($request->all());

    $this->modelService->handleUploadedImage($model->id);

    return redirect('/cars/models')->with('message', __('car.model_added'));
}

6

О чем еще не должен знать контроллер? Правильно, о конкретном URI, который может быть изменен в любое время. Вместо него мы будем использовать название маршрута.

После всех модификаций, наш контроллер делает только то, что должен и вообще, выглядит просто шикарно:

public function store(ModelRequest $request)
{
    $model = $this->model->create($request->all());

    $this->modelService->handleUploadedImage($model->id);

    return redirect()->route('models.index')->with('message', __('car.model_added'));
}

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

Если хотите увидеть больше подобных статей, ставьте звезды в репозитории хороших практик Laravel. Это показывает есть ли интерес к этой теме, а также здорово мотивирует автора на написание других полезных материалов.

Обсуждение хороших практик Laravel

Как вы считаете, полезен ли этот материал? Да Нет

Комментарии (13)

covobo

Рефактор получился не плохим.

Добавлю свое представление: метод handleUploadedImage следует видоизменить, передавать ему не Request, а непосредственно File (иначе как можно переиспользовать этот метод?).
А при config('app.uploading_enabled') == false, метод handleUploadedImage должен выкидывать Exception (ибо метод был вызван, но выполниться он не может, это как-то странно, что он молча ничего не сделает).

AlexeyMezenin

Спасибо за дополнение.

Переиспользовать метод можно, ведь в любом случае этот метод зависит от данных в объекте Request. Тестируется это тоже отлично. Плюс же заключается в том, что мы не передает множество данных, а просто внедряем Request. Если бы мы работали с моделью или другими классами, там да, нужно передавать непосредственно данные (массив, создаваемый $request->all()), хотя многие передают объект Request.

На счет исключения, я стараюсь использовать их только там, где они действительно необходимы. В данном случае нам не нужно знать есть ли там вообще изображение или нет. В более сложном случае подход, описанный тобой, безусловно выглядит лучше.

Если у тебя есть дополнения к описанным практикам Laravel, буду благодарен, если поделишься ими здесь

Proger_XP

Отличная статья, и написана хорошо (только в заголовках не принято ставить точки в конце, лучше это исправить).

Однако раз текст претендует на «beyond junior», т.е. на уровень повыше начинающего, то надо предупредить о том, что чрезмерное размазывание кода по разным сущностям — это такое же зло, как и пихание всего в один класс (контроллер в данном случае).

К сожалению, известные мне фреймворки поощряют и то, и другое, причём сложно сказать, какое из зол меньше — при размазанном коде много времени тратится на поиск того, где все-таки выполняется код (чем грешит и сам Laravel со своей тучей фасадов и IoC).

Как всегда, никакие практики и принципы не заменяют собственной головы на плечах.

AlexeyMezenin

Спасибо, точки убрал.

Не уверен на счет размазывания кода, не думаю, что видел такое в приложениях. Наоборот, весь код обычно в контроллерах. В моем примере присутствует размазывание?

Ed

Мне кажется вы не замечаете главной причины возникновения проблемы плохик практик. Возможно и я заблуждаюсь, но как настоящий новичек (php «со словарем» → сделал себе блог; не теряюсь в html и css) выскажу свое мнение.

Прочитав всю документацию по ларавел на русском, а затем и на английском для последней версии, плюс еще с десяток статей, мало что понял. Общую картинку получил, но как начать что-то делать все же было не ясно. В итоге вернулся к проверенному способу дающему результат «делай как я», а именно смотря на ютубе ролики от пары авторов начал писать блог, а затем менеджер проектов. К сожалению их уроки еще в процессе съемок, так что дальше приходится самостоятельно.

Так вот проблема в том, что как оказалось, эти уроки и учат плохим практикам. Желая писать правильно прочел несколько статей подобной вашей. Но! опять мало что понял(( Т.е. явно не для новичков материал. Для новичков другой подход нужен. Хоть многие и смеются «очередной блог на ларавел», но это именно то, на чем учатся!

Когда дело доходит до понимания того, что пишите вы, уже поздно. Уже привык писать не правильно. Нужно переучиваться, а это всегда проблематично, ведь проще и быстрее написать как знаешь.

Сделайте хоть кто-нибудь полный урок, начиная с кода «как правильно создать запись в БД на ларавел», а попутно придется объяснить и про политики и про сервис котроллеры.

AlexeyMezenin

Я всерьез думал об этом когда ко мне обратилась менеджер Packt с предложением написать самоучитель по Laravel. Много думал и пришел к выводу, что писать очередную книгу смысла нет, а написать книгу для новичков и сразу по ходу объяснять «как правильно делать» ну очень сложно. Человек так устроен, что он не может впитать слишком много информации за короткий промежуток времени, поэтому лучше всего начать обучение с обычной книги для новичков или видеокастов, а уже потом расти. Для меня этот продход работает.

А переучиться не так сложно. Все мы когда-то писали полную жесть и будущие мы будут считать, что сейчас мы тоже пишем убогий код. Мы все время растем, это нормально.

Ed

Убедили, буду продолжать) Благо в гугле почти все можно найти. Хотя с хомстедом промучался долго, несмотря на кучу мануалов по его установке. А еще с Mix, тоже было не просто, т.к. упорно не ставился npm на хомстеде, в итоге стал с ошибками, но скомпилил таки css.

С php было проще, общую логику понял и пишу себе поглядывая в гугле как делается очередной необходимый кусочек функционала. Тут же слишком много всего, куча пакетов с кучей версий, которые не так просто подключить (как было с mix), а главное еще нужно понять какой для чего реально нужен. Много статей прочитал для начинающих, и во многих сказано: «ставим это и то и еще это», а вот для чего не сказано. И в итоге, чтобы запилить «хелло ворлд» папка с проектом вырастает до 200Мб непонятно чего))
Может не учебник, а хотябы упорядоченый, толковый и желательно актуальный (под 5.5) список ссылок на готовые статьи по теме соберете?) Начиная с первой про среду разработки и разбор базовых и реально необходимых доп.пакетов... А то ведь найти можно все, но отличить полезное от вредного в найденном новичку не под силу обычно.

(если честно я не знаю зачем вам тратить на это время, но раз уж вы подобное делаете, я готов нагло воспользоваться)))

AlexeyMezenin

Спасибо за идею, если будет время, займусь. А пока, вот такой вот репозиторий есть.

Adobe

Я как-то имел неосторожность подобную тему затронуть в группе laravel, меня чуть говном не закидали за такую дерзость. А что вы имеете против html кода в контроллерах? А может вы еще и от AR отступились? Барбара Лисков, кто она вообще такая и что она себе позволяет.

AlexeyMezenin

Понимаю, тоже часто с подобным отношением сталкиваюсь. Не все понимают важность использования хороших практик для создания поддерживаемых приложений. Если есть дополнения к практикам в репозитории, буду рад о них услышать. )

Proger_XP
  1. А может вы еще и от AR отступились?

Мне кажется, вы вообще коммунист.

Падение уровня программистов — неизбежное следствие снижения порога входа. «Попсовый Laravel», так сказать.

kaa

exists:makes.id должна быть запятая exists:makes,id

AlexeyMezenin

Спасибо, исправил.

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.