Це не адаптація Laravel під принципи SOLID, схем тощо. Тут ви знайдете найкращі практики, які зазвичай ігнорують в справжніх Laravel проєктах. Також, рекомендую ознайомитися з хорошими практиками в контексті PHP.
Принцип єдиної відповідальності (Single responsibility principle)
Товсті моделі, тонкі контролери
Бізнес-логіка лише в сервісних класах
Ніяких запитів у шаблонах Blade та використовуйте жадібне завантаження (проблема N + 1)
Коментуйте свій код, але описові назви методів та змінних краще
Жодних JS та CSS у шаблонах Blade та HTML у PHP класах
Конфігурації, мовні файли та константи замість тексту в коді
Використовуйте стандартні інструменти Laravel, що прийняті спільнотою
Дотримуйтеся домовленостей Laravel з найменування
Використовуйте, де можливо, короткий та читабельний синтаксис
Використовуйте контейнер IoC або фасади замість new Class
Не отримуйте дані безпосередньо з файлу .env
Клас та метод повинні мати лише одну відповідальність.
Погано:
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;
}
}
Добре:
public function getFullNameAttribute()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient()
{
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 запитами.
Погано:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
Добре:
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 класах, а не контролерах.
Погано:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
....
}
Добре:
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',
];
}
}
Контролер має виконувати свої прямі обов’язки, тож перемістіть бізнес-логіку з контролерів до сервісних класів.
Bad:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}
....
}
Good:
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');
}
}
}
Повторно використовуйте код де можете. SRP вже допомагатиме вам уникати задвоєнь, але Laravel дозволяє також повторно використовувати шаблони Blade, області дії Eloquent тощо.
Погано:
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();
}
Добре:
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 та сирих SQL запитів. Перевага у колекцій, а не масивів
Eloquent дозволяє вам писати читабельний та підтримний код. Також, Eloquent має чудові вбудовані інструменти, як-от: м’які видалення, події, області дії тощо.
Погано:
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
Добре:
Article::has('user.profile')->verified()->latest()->get();
Погано:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Додати категорію до статті
$article->category_id = $category->id;
$article->save();
Добре:
$category->article()->create($request->validated());
Погано (на 100 користувачів 101 запит у БД (базу даних)):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Добре (на 100 користувачів лише 2 запити у БД):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
Погано:
if (count((array) $builder->getQuery()->joins) > 0)
Нормально:
// Визначає наявність join-ів.
if (count((array) $builder->getQuery()->joins) > 0)
Добре:
if ($this->hasJoins())
Погано:
let article = `{{ json_encode($article) }}`;
Краще:
<input id="article" type="hidden" value='@json($article)'>
Або
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>
У файлі Javascript:
let article = $('#article').val();
Найкращий варіант — використовувати спеціалізований пакунок для передачі даних з PHP до JS.
Погано:
public function isNormal()
{
return $article->type === 'normal';
}
return back()->with('message', 'Вашу статтю було додано!');
Добре:
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}
return back()->with('message', __('app.article_added'));
Віддавайте перевагу вбудованому функціоналу Laravel та пакункам від спільноти використанню сторонніх пакунків та інструментів. Будь-якому розробнику, що працюватиме з вашим застосунком у майбутньому, знадобиться вивчати нові інструменти. Окрім того, в разі використання сторонніх пакунків чи інструментів шанси отримати допомогу від спільноти Laravel відчутно менші. Не змушуйте свого клієнта платити за це.
Завдання | Стандартні інструменти | Сторонні інструменти |
---|---|---|
Авторизація | Policies | Entrust, Sentinel and other packages |
Компіляція засобів | Laravel Mix | Grunt, Gulp, 3rd party packages |
Середовище розробки | Laravel Sail, Homestead | Docker |
Розгортання застосунків | Laravel Forge | Deployer and other solutions |
Unit тестування | PHPUnit, Mockery | Phpspec |
Тестування браузера | Laravel Dusk | Codeception |
База даних | Eloquent | SQL, Doctrine |
Шаблони | Blade | Twig |
Робота з даними | Laravel collections | Arrays |
Перевірка даних форми | Request classes | 3rd party packages, validation in controller |
Автентифікація | Built-in | 3rd party packages, your own solution |
API автентифікація | Laravel Passport, Laravel Sanctum | 3rd party JWT and OAuth packages |
Створення API | Built-in | Dingo API and similar packages |
Робота зі структурою БД | Migrations | Working with DB structure directly |
Локалізація | Built-in | 3rd party packages |
Користувацькі інтерфейси в реальному часі | Laravel Echo, Pusher | 3rd party packages and working with WebSockets directly |
Генерування тестових даних | Seeder classes, Model Factories, Faker | Creating testing data manually |
Планування завдань | Laravel Task Scheduler | Scripts and 3rd party packages |
База даних | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
Дотримуйтеся стандартів PSR.
Також, дотримуйтеся домовленостей з найменування прийнятих спільнотою Laravel:
Що | Написання | Добре | Погано |
---|---|---|---|
Контролер | однина | ArticleController | |
Маршрути | множина | articles/1 | |
Назви маршрутів | snake_case з позначенням крапкою | users.show_active | |
Модель | однина | User | |
Зв’язки hasOne або belongsTo | однина | articleComment | |
Решта зв’язків | множина | articleComments | |
Таблиця | множина | article_comments | |
Зведена таблиця | ім’я моделі в однині в алфавітному порядку | article_user | |
Стовпчик таблиці | snake_case без імені моделі | meta_title | |
Властивість моделі | snake_case | $model->created_at | |
Зовнішній ключ | ім’я моделі в однині з суфіксом _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 | |
Вигляд | kebab-case | show-filtered.blade.php | |
Конфігурація | snake_case | google_calendar.php | |
Домовленість (інтерфейс) | прикметник або іменник | AuthenticationInterface | |
Trait | прикметник | Notifiable |
Погано:
$request->session()->get('cart');
$request->input('name');
Добре:
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::get('name') |
$request->name, request('name') |
return Redirect::back() |
return back() |
is_null($object->relation) ? null : $object->relation->id |
optional($object->relation)->id |
return view('index')->with('title', $title)->with('client', $client) |
return view('index', compact('title', 'client')) |
$request->has('value') ? $request->value : 'default'; |
$request->get('value', 'default') |
Carbon::now(), Carbon::today() |
now(), today() |
App::make('Class') |
app('Class') |
->where('column', '=', 1) |
->where('column', 1) |
->orderBy('created_at', 'desc') |
->latest() |
->orderBy('age', 'desc') |
->latest('age') |
->orderBy('created_at', 'asc') |
->oldest() |
->select('id', 'name')->get() |
->get(['id', 'name']) |
->first()->name |
->value('name') |
Синтаксис new Class створює міцні з’єднання між класами та ускладнює тестування. Використовуйте натомість контейнер IoC або фасади.
Погано:
$user = new User;
$user->create($request->validated());
Добре:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
Натомість, передавайте дані до конфігураційних файлів та потім використовуйте допоміжну функцію config()
для використання даних в застосунку.
Погано:
$apiKey = env('API_KEY');
Добре:
// config/api.php
'key' => env('API_KEY'),
// Використайте дані
$apiKey = config('api.key');
Зберігайте дати в стандартному форматі. Використовуйте методи доступу та зміни даних для зміни формату
Погано:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Добре:
// Модель
protected $dates = ['ordered_at', 'created_at', 'updated_at'];
public function getSomeDateAttribute($date)
{
return $date->format('m-d');
}
// Вигляд
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
Жодної логіки в маршрутах.
Зменшіть використання чистого PHP у шаблонах Blade.