💡はじめに
この記事では、私が実務で使用しているLaravelのベストプラクティスを具体的なコード例とあわせていくつか紹介していきます。
👍こんな人におすすめ
- Laravelを一通り触ったことがある方
- 設計や可読性を意識したいと考えている人
🚩使用目的
Laravelは機能が豊富で柔軟性も高く、少ないコードでサクッとアプリを作れるのが大きな魅力です。
ただ、その便利さゆえにコントローラが肥大化してしまったり、ビジネスロジックがあちこちに散らばってしまったりと、コードの品質や保守性に課題が出やすいのも事実です。
プロジェクトが大きくなるにつれて可読性や拡張性が失われていき、バグや開発効率の低下につながることもあります。
だからこそ最初から品質や保守性を意識した書き方を取り入れることが大事だと感じています。
単一責任の原則
クラスとメソッドは1つの責任だけを持つべきです。
NG:
public function getFullNameAttribute(): string
{
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;
}
}
OK:
public function getFullNameAttribute(): string
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient(): bool
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}
public function getFullNameLong(): string
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}
public function getFullNameShort(): string
{
return $this->first_name[0] . '. ' . $this->last_name;
}
ファットモデル、スキニーコントローラ
DBに関連するすべてのロジックはEloquentモデルに入れるか、クエリビルダもしくは生のSQLクエリを使用する場合はレポジトリークラスに入れます。
NG:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
OK:
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();
}
}
バリデーション
バリデーションはコントローラからリクエストクラスに移動させます。
NG:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
...
}
OK:
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',
];
}
}
繰り返し書かない (DRY)
可能であればコードを再利用します。単一責任の原則は重複を避けることに役立ちます。また、Bladeテンプレートを再利用したり、Eloquentのスコープなどを使用したりします。
NG:
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();
}
OK:
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();
}
JSとCSSをBladeの中に書かない、PHPクラスの中にHTMLを入れない
NG:
let article = `{{ json_encode($article) }}`;
OK:
<input id="article" type="hidden" value='@json($article)'>
Or
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>
JavaScript ファイルで以下のように記述します:
let article = $('#article').val();
もっとも良い方法は、データを転送するためJSパッケージに特別なPHPを使用することです。
クエリビルダや生のSQLクエリよりもEloquentを優先して使い、配列よりもコレクションを優先
Eloquentにより読みやすくメンテナンスしやすいコードを書くことができます。また、Eloquentには論理削除、イベント、スコープなどの優れた組み込みツールがあります。
NG:
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
OK:
Article::has('user.profile')->verified()->latest()->get();
マスアサインメント
NG:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();
OK:
$category->article()->create($request->validated());
Blade内でクエリを実行しない(N+1問題)
NG: 100ユーザに対して、101回のDBクエリが実行される
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
OK: 100ユーザに対して、2回のDBクエリが実行される
$users = User::with('profile')->get();
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
できるだけ短く読みやすい構文で書く
NG:
$request->session()->get('cart');
$request->input('name');
OK:
session('cart');
$request->name;
Laravelの命名規則に従う
PSRに従います。
また、Laravelコミュニティに受け入れられた命名規則に従います。
| 対象 | 規則 | Good | Bad |
|---|---|---|---|
| コントローラ | 単数形 | ArticleController | ArticlesController |
| ルート | 複数形 | articles/1 | article/1 |
| 名前付きルート | スネークケースとドット表記 | users.show_active | users.show-active,show-active-users |
| モデル | 単数形 | User | Users |
| hasOneまたはbelongsTo関係 | 単数形 | articleComment | articleComments,article_comment |
| テーブル | 複数形 | article_comments | article_comment,articleComments |
| テーブルカラム | スネークケース モデル名は含めない | meta_title | MetaTitle,article_meta_title |
| メソッド | キャメルケース | getAll | get_all |
| 変数 | キャメルケース | $articlesWithAuthor | $articles_with_author |
| ビュー | ケバブケース | show-filtered.blade.php | showFiltered.blade.php |
| コンフィグ | スネークケース | google_calendar.php | googleCalendar.php |
.envファイルのデータを直接参照しない
代わりにconfigファイルへデータを渡します。そして、アプリケーション内でデータを参照する場合はconfig()ヘルパー関数を使います。
NG:
$apiKey = env('abc123');
OK:
// config/api.php
'key' => env('API_KEY'),
// データを使用する
$apiKey = config('api.key');
まとめ
この記事では、私が実務で意識しているLaravelのベストプラクティスをコード例とともに紹介しました。
- 単一責任の原則を意識して、コードを小さく整理する
- ファットモデル・スキニーコントローラ で責務を明確にする
- バリデーションをRequestディレクトリ配下へ移動して再利用性を高める
- DRYの原則でコードの重複を避ける
- Eloquentの活用とN+1問題の回避で読みやすくパフォーマンスの良い処理を書く
- 可読性を意識した短い構文や命名規則を徹底する
- 設定値は.envから直接呼ばずconfigを経由させる