##Небольшое вступление В мире Laravel существует очень серьезная, на мой взгляд, проблема. Laracasts, книги, видео туториалы, статьи и даже документация показывают нам использование плохих практик. Понятно, что делается это для популяризации фреймворка, снижая порог вхождения для новичков. Действительно, благодаря такому подходу, человек может написать работающее веб приложение при минимальных усилиях. И это хорошо. Плохо то, что разработчик продолжает писать низкокачественный код даже в сложных приложениях, в результате чего они порой становятся абсолютно неподдерживаемыми. Это значит, что любое изменение функционала в таком приложении занимает в разы, а иногда и в десятки раз больше времени разработчика и, соответственно, денег клиента. Также, эти правки образуют новые баги, ломают другие части приложения и т.д. Проанализировав десятки реальных коммерческих приложений, написанных на Laravel, я понял, что практически все разработчики игнорируют даже самые простые хорошие практики написания кода. Отталкиваться я буду не от заезженных терминов вроде DRY, SOLID, описания паттернов и пр. Подобных статей довольно много и их авторы, зачастую не понимая сути подобных принципов, вновь и вновь пересказывают их. А примеры кода в этих статьях зачастую не имеют ничего общего с кодом приложений из реального мира. Моя же цель - дать новичкам действительно полезный материал, с помощью которого они бы могли заметно улучшить качество своих приложений. ##Принцип единственной ответственности (Single responsibility principle или SRP) Хотите писать более качественный и поддерживаемый код, чем пишет 95% Laravel разработчиков? Изучите, впитайте и везде применяйте принцип единственной ответственности (далее SRP). Вы также можете прочитать о других [хороших практиках Laravel](https://github.com/alexeymezenin/laravel-best-practices/blob/master/russian.md), а также о [хороших практиках разработки в контексте PHP](https://github.com/jupeter/clean-code-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 говорит нам о том, что этот метод контроллера должен выполнять лишь одну функцию. Контроллер должен лишь обработать запрос и отправить ответ. В результате нарушения этого важного принципа, контроллеры раздуваются до невероятных размеров и вносить изменения в существующий код, не сломав его, становится все сложнее и сложнее. Тестировать такой код также намного труднее, а иногда и вовсе невозможно. Давайте также вспомним о концепции [тонких контроллеров](https://ru.wikipedia.org/wiki/Model-View-Controller#.D0.9D.D0.B0.D0.B8.D0.B1.D0.BE.D0.BB.D0.B5.D0.B5_.D1.87.D0.B0.D1.81.D1.82.D1.8B.D0.B5_.D0.BE.D1.88.D0.B8.D0.B1.D0.BA.D0.B8) и начнем рефакторинг. ###1 Контроллер не должен заниматься валидацией, поэтому первое, что мы сделаем - [создадим Request-класс](https://laravel.ru/docs/v5/validation#создание_запроса) и перенесем валидацию в него. Также обратите внимание на этот кусок кода: $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](https://laravel.ru/docs/v5/container), мы внедрим и будем использовать сервис-класс в контроллере, после чего последний будет выглядеть так: 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 Должен ли контроллер делать проверку прав пользователей? Конечно же нет. Поэтому перенесем эту логику туда, где ей самое место: в [политиках](https://laravel.ru/docs/v5/authorization#написание_политик). class ModelPolicy { public function create(User $user) { return isModerator() && auth()->user()->canAddContent(); } } Вызывать логику авторизации возможно либо из файла маршрутов, либо из контроллера: $this->authorize('create', Model::class); ###5 В оригинальном коде мы имели два сообщения об ошибках и одно сообщение об успешном выполнении метода. После рефакторинга о сообщениях об ошибках будет заботиться уже Request-класс. Контроллер "не должен знать" о конкретном тексте, поэтому давайте также вынесем последнее сообщение в [языковой файл](https://laravel.ru/docs/v5/localization). Теперь наш контроллер выглядит так: public function store(ModelRequest $request) { $this->authorize('create', Model::class); $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) { $this->authorize('create', Model::class); $model = $this->model->create($request->all()); $this->modelService->handleUploadedImage($model->id); return redirect()->route('models.index')->with('message', __('car.model_added')); } Вот и все. Теперь, если клиент попросит нас внести какие-либо изменения в действующий функционал, мы будем уверены, что сделаем это быстро и ничего при этом не сломаем. Если хотите увидеть больше подобных статей, ставьте звезды в [репозитории хороших практик Laravel](https://github.com/alexeymezenin/laravel-best-practices/blob/master/russian.md). Это показывает есть ли интерес к этой теме, а также здорово мотивирует автора на написание других полезных материалов. [Обсуждение хороших практик Laravel](https://laravel.ru/forum/viewforum.php?id=17)