Laravel 5
Хорошие практики в Laravel
Источник: https://github.com/alexeymezenin/laravel-best-practices
Это не пересказ лучших практик вроде SOLID, паттернов и пр. с адаптацией под Laravel. Здесь собраны именно практики, которые игнорируются в реальных Laravel проектах. Также, рекомендую ознакомитсья с хорошими практиками в контексте PHP. Смотрите также обсуждение хороших практик Laravel.
Принцип единственной ответственности (Single responsibility principle)
Каждый класс и метод должны выполнять лишь одну функцию.
Плохо:
1 2 3 4 5 6 7 8 9 |
public function getFullNameAttribute() { if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) { return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' $this->last_name; } else { return $this->first_name[0] . '. ' . $this->last_name; } } |
Хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public function getFullNameAttribute() { return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort(); } public function isVerfiedClient() { return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified(); } public function getFullNameLong() { return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; } public function getFullNameShort() { return $this->first_name[0] . '. ' . $this->last_name; } |
Тонкие контроллеры, толстые модели
По своей сути, это лишь один из частных случаев принципа единой ответственности. Выносите работу с данными в модели при работе с Eloquent или в репозитории при работе с Query Builder или “сырыми” SQL запросами.
Плохо:
1 2 3 4 5 6 7 8 9 10 11 |
public function index() { $clients = Client::verified() ->with(['orders' => function ($q) { $q->where('created_at', '>', Carbon::today()->subWeek()); }]) ->get(); return view('index', ['clients' => $clients]); } |
Хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function index() { return view('index', ['clients' => $this->client->getWithNewOrders()]); } Class Client extends Model { public function getWithNewOrders() { return $this->verified() ->with(['orders' => function ($q) { $q->where('created_at', '>', Carbon::today()->subWeek()); }]) ->get(); } } |
Валидация
Следуя принципам тонкого контроллера и SRP, выносите валидацию из контроллера в Request классы.
Плохо:
1 2 3 4 5 6 7 8 9 10 11 |
public function store(Request $request) { $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', 'publish_at' => 'nullable|date', ]); .... } |
Хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function store(PostRequest $request) { .... } class PostRequest extends Request { public function rules() { return [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', 'publish_at' => 'nullable|date', ]; } } |
Бизнес логика в сервис-классах
Контроллер должен выполнять только свои прямые обязанности, поэтому выносите всю бизнес логику в отдельные классы и сервис классы.
Плохо:
1 2 3 4 5 6 7 8 9 |
public function store(Request $request) { if ($request->hasFile('image')) { $request->file('image')->move(public_path('images') . 'temp'); } .... } |
Хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function store(Request $request) { $this->articleService->handleUploadedImage($request->file('image')); .... } class ArticleService { public function handleUploadedImage($image) { if (!is_null($image)) { $image->move(public_path('images') . 'temp'); } } } |
Не повторяйся (DRY)
Этот принцип призывает вас переиспользовать код везде, где это возможно. Если вы следуете принципу SRP, вы уже избегаете повторений, но Laravel позволяет вам также переиспользовать представления, части Eloquent запросов и т.д.
Плохо:
1 2 3 4 5 6 7 8 9 10 11 12 |
public function getActive() { return $this->where('verified', 1)->whereNotNull('deleted_at')->get(); } public function getArticles() { return $this->whereHas('user', function ($q) { $q->where('verified', 1)->whereNotNull('deleted_at'); })->get(); } |
Хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function scopeActive($q) { return $q->where('verified', 1)->whereNotNull('deleted_at'); } public function getActive() { return $this->active()->get(); } public function getArticles() { return $this->whereHas('user', function ($q) { $q->active(); })->get(); } |
Предпочитайте Eloquent конструктору запросов (query builder) и сырым запросам в БД. Предпочитайте работу с коллекциями работе с массивами
Eloquent позволяет писать максимально читаемый код, а изменять функционал приложения несоизмеримо легче. У Eloquent также есть ряд удобных и мощных инструментов.
Плохо:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
SELECT * FROM `articles` WHERE EXISTS (SELECT * FROM `users` WHERE `articles`.`user_id` = `users`.`id` AND EXISTS (SELECT * FROM `profiles` WHERE `profiles`.`user_id` = `users`.`id`) AND `users`.`deleted_at` IS NULL) AND `verified` = '1' AND `active` = '1' ORDER BY `created_at` DESC |
Хорошо:
1 2 |
Article::has('user.profile')->verified()->latest()->get(); |
Используйте массовое заполнение (mass assignment)
Плохо:
1 2 3 4 5 6 7 8 |
$article = new Article; $article->title = $request->title; $article->content = $request->content; $article->verified = $request->verified; // Привязать статью к категории. $article->category_id = $category->id; $article->save(); |
Хорошо:
1 2 |
$category->article()->create($request->all()); |
Не выполняйте запросы в представлениях и используйте нетерпеливую загрузку (проблема N + 1)
Плохо (будет выполнен 101 запрос в БД для 100 пользователей):
1 2 3 4 |
@foreach (User::all() as $user) {{ $user->profile->name }} @endforeach |
Хорошо (будет выполнено 2 запроса в БД для 100 пользователей):
1 2 3 4 5 6 7 8 |
$users = User::with('profile')->get(); ... @foreach ($users as $user) {{ $user->profile->name }} @endforeach |
Комментируйте код, предпочитайте читаемые имена методов комментариям
Плохо:
1 2 |
if (count((array) $builder->getQuery()->joins) > 0) |
Лучше:
1 2 3 |
// Determine if there are any joins. if (count((array) $builder->getQuery()->joins) > 0) |
Хорошо:
1 2 |
if ($this->hasJoins()) |
Выносите JS и CSS из шаблонов Blade и HTML из PHP кода
Плохо:
1 2 |
let article = `{{ json_encode($article) }}`; |
Лучше:
1 2 3 4 5 6 |
<input id="article" type="hidden" value="{{ json_encode($article) }}"> Или <button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button> |
В Javascript файле:
1 2 |
let article = $('#article').val(); |
Еще лучше использовать специализированный пакет для передачи данных из бэкенда во фронтенд.
Конфиги, языковые файлы и константы вместо текста в коде
Непосредственно в коде не должно быть никакого текста.
Плохо:
1 2 3 4 5 6 7 |
public function isNormal() { return $article->type === 'normal'; } return back()->with('message', 'Ваша статья была успешно добавлена'); |
Хорошо:
1 2 3 4 5 6 7 |
public function isNormal() { return $article->type === Article::TYPE_NORMAL; } return back()->with('message', __('app.article_added')); |
Используйте инструменты и практики принятые сообществом
Laravel имеет встроенные инструменты для решения часто встречаемых задач. Предпочитайте пользоваться ими использованию сторонних пакетов и инструментов. Laravel разработчику, пришедшему в проект после вас, придется изучать и работать с новым для него инструментом, со всеми вытекающими последствиями. Получить помощь от сообщества будет также гораздо труднее. Не заставляйте клиента или работодателя платить за ваши велосипеды.
Задача | Стандартные инструмент | Нестандартные инструмент |
---|---|---|
Авторизация | Политики | Entrust, Sentinel и др. пакеты, собственное решение |
Работа с JS, CSS и пр. | Laravel Mix | Grunt, Gulp, сторонние пакеты |
Среда разработки | Homestead | Docker |
Разворачивание приложений | Laravel Forge | Deployer и многие другие |
Тестирование | Phpunit, Mockery | Phpspec |
e2e тестирование | Laravel Dusk | Codeception |
Работа с БД | Eloquent | SQL, построитель запросов, Doctrine |
Шаблоны | Blade | Twig |
Работа с данными | Коллекции Laravel | Массивы |
Валидация форм | Request классы | Сторонние пакеты, валидация в контроллере |
Аутентификация | Встроенный функционал | Сторонние пакеты, собственное решение |
Аутентификация API | Laravel Passport | Сторонние пакеты, использующие JWT, OAuth |
Создание API | Встроенный функционал | Dingo API и другие пакеты |
Работа со структурой БД | Миграции | Работа с БД напрямую |
Локализация | Встроенный функционал | Сторонние пакеты |
Обмен данными в реальном времени | Laravel Echo, Pusher | Пакеты и работа с веб сокетами напрямую |
Генерация тестовых данных | Seeder классы, фабрики моделей, Faker | Ручное заполнение и пакеты |
Планирование задач | Планировщик задач Laravel | Скрипты и сторонние пакеты |
БД | MySQL, PostgreSQL, SQLite, SQL Server | MongoDb |
Соблюдайте соглашения сообщества об именовании
Следуйте стандартам PSR при написании кода.
Также, соблюдайте другие cоглашения об именовании:
Что | Правило | Принято | Не принято |
---|---|---|---|
Контроллер | ед. ч. | ArticleController | |
Маршруты | мн. ч. | articles/1 | |
Имена маршрутов | snake_case | users.show_active | |
Модель | ед. ч. | User | |
Отношения hasOne и belongsTo | ед. ч. | articleComment | |
Все остальные отношения | мн. ч. | articleComments | |
Таблица | мн. ч. | article_comments | |
Pivot таблица | имена моделей в алфавитном порядке в ед. ч. | article_user | |
Столбец в таблице | snake_case без имени модели | meta_title | |
Внешний ключ | имя модели ед. ч. и _id | article_id | |
Первичный ключ | – | id | |
Миграция | – | 2017_01_01_000000_create_articles_table | |
Метод | camelCase | getAll | |
Метод в контроллере ресурсов | таблица | store | |
Метод в тесте | camelCase | testGuestCannotSeeArticle | |
Переменные | camelCase | $articlesWithAuthor | |
Коллекция | описательное, мн. ч. | $activeUsers = User::active()->get() | |
Объект | описательное, ед. ч. | $activeUser = User::active()->first() | |
Индексы в конфиге и языковых файлах | snake_case | articles_enabled | |
Представление | snake_case | show_filtered.blade.php | |
Конфигурационный файл | snake_case | google_calendar.php | |
Контракт (интерфейс) | прилагательное или существительное | Authenticatable | |
Трейт | прилагательное | Notifiable |
Короткий и читаемый синтаксис там, где это возможно
Плохо:
1 2 3 |
$request->session()->get('cart'); $request->input('name'); |
Хорошо:
1 2 3 |
session('cart'); $request->name; |
Еще примеры:
Часто используемый синтаксис | Более короткий и читаемый синтаксис |
---|---|
Session::get('cart') |
session('cart') |
$request->session()->get('cart') |
session('cart') |
Session::put('cart', $data) |
session(['cart' => $data]) |
$request->input('name') |
$request->name |
Request::get('name') |
request('name') |
return Redirect::back() |
return back() |
return view('index')->with('title', $title)->with('client', $client) |
return view('index', compact('title', 'client')) |
Используйте IoC или фасады вместо new Class
Внедрение классов через синтаксис new Class создает сильное сопряжение между частями приложения и усложняет тестирование. Используйте контейнер или фасады.
Плохо:
1 2 3 |
$user = new User; $user->create($request->all()); |
Хорошо:
1 2 3 4 5 6 7 8 9 |
public function __construct(User $user) { $this->user = $user; } .... $this->user->create($request->all()); |
Другие советы и практики
Не размещайте логику в маршрутах.
Старайтесь не использовать сырой PHP в шаблонах Blade.