はじめに
Laravelベストプラクティスを改めて読んでどのプロジェクトでも認識すべきルールを軸にまとめてみました
逆に「プロジェクトによって運用方法が変わる」ような箇所は割愛した箇所は後述でリストにしてみました
Laravelベストプラクティスの目的
Laravelは強力で使いやすいPHPフレームワークですが、
ベストプラクティスに従うことで、その真価を最大限に引き出すことができます。
このガイドでは、Laravelのコーディングスタイルや設計パターンにおけるベストプラクティスを紹介し、コードの品質向上、保守性の向上、そして開発効率の向上を目指します。
単一の原則
クラスとメソッドは1つの責任だけを持つべきです。
Bad:
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;
}
}
Good:
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クエリを使用する場合はレポジトリークラスに入れます
Bad:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
Good:
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();
}
}
バリデーション
バリデーションはコントローラからリクエストクラスに移動させます
Bad:
class Controller extends Controller
{
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
...
}
}
Good:
class Controller extends Controller
{
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つの責務だけを持たないといけません、
そのためビジネスロジックはコントローラからサービスクラスに移動させます
リクエストを受け取ってViewなりjsonなりを別の処理へ渡すのがコントローラの責務ですので
それ以外の機能的な部分はサービスクラスで実装しようねって話ですね
Bad:
class Controller extends Controller
{
public function store(PostRequest $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}
...
}
}
Good:
class Controller extends Controller
{
public function store(PostRequest $request)
{
$this->articleService->handleUploadedImage($request->file('image'));
...
}
}
class ArticleService
{
public function handleUploadedImage($image)
{
if (!is_null($image)) {
$image->move(public_path('images') . 'temp');
}
}
}
繰り返し書かない (DRY)
可能であればコードを再利用します
単一責任の原則は重複を避けることに役立ちます
Bad:
// 下記の->where('verified', 1)->whereNotNull('deleted_at')->が重複してしまっている
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();
}
Good:
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();
}
クエリビルダや生のSQLクエリよりもEloquentを優先して使い、配列よりもコレクションを優先する
Eloquentを使うことで、読みやすくメンテナンスしやすいコードを書けます
Bad:
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
Good:
Article::has('user.profile')->verified()->latest()->get();
マスアサインメント
Eloquentのマスアサインメント機能を使ってデータの保存を簡潔にします。
Bad:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
$article->category_id = $category->id;
$article->save();
Good:
$category->article()->create($request->validated());
Bladeテンプレート内でクエリを実行しない。Eager Lodingを使う(N + 1問題)
クエリの効率化のため、Eager Loadingを使用します
Bad (100ユーザに対して、101回のDBクエリが実行される):
@foreach (User::all() /** ←で1回 **/ as $user)
{{ $user->profile->name }} /** リレーションを使わなければユーザーの数だけprofileテーブルを参照する 100ユーザーいれば100回 **/
@endforeach
Good (100ユーザに対して、2回のDBクエリが実行される):
// 最初のクエリで全ユーザーを取得し、2回目のクエリで各ユーザーのプロファイルを一度に取得する
$users = User::with('profile')->get();
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
コメントを書く。ただしコメントよりも説明的なメソッド名と変数名を付けるほうが良い
コメントを適切に使いつつ、コード自体が説明的であるようにします
Bad:
if (count((array) $builder->getQuery()->joins) > 0)
Better:
// 参加者がいるかどうかを判断する
if (count((array) $builder->getQuery()->joins) > 0)
Good:
if ($this->hasJoins())
JSとCSSをBladeテンプレートの中に入れない、PHPクラスの中にHTMLを入れない
Bad:
let article = `{{ json_encode($article) }}`;
Better:
<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();
コード内の文字列の代わりにconfigファイルとlanguageのファイル、定数を使う
ハードコーディングされた文字列を避け、設定ファイルや定数を使用します
Bad:
public function isNormal()
{
return $article->type === 'normal'; // ここを定数にする
}
return back()->with('message', 'Your article has been added!');
Good:
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}
return back()->with('message', __('app.article_added'));
できるだけ短く読みやすい構文で書く
Laravelのヘルパー関数を活用してコードを簡潔にします
Bad:
$request->session()->get('cart');
$request->input('name');
Good:
session('cart');
$request->name;
newの代わりにIoCコンテナもしくはファサードを使う
IoCコンテナやファサードを使って依存関係を管理し、テストを容易にします
Bad:
$user = new User;
$user->create($request->validated());
Good:
public function __construct(User $user)
{
$this->user = $user;
}
...
$this->user->create($request->validated());
.envファイルのデータを直接参照しない
設定ファイルを通じて環境変数を参照し、アプリケーション内でconfig()ヘルパー関数を使用します
Bad:
$apiKey = env('API_KEY');
Good:
// config/api.php
'key' => env('API_KEY'),
// データを使用する
$apiKey = config('api.key');
日付を標準フォーマットで保存する。アクセサとミューテータを使って日付フォーマットを変更する
日付フォーマットの管理を容易にするため、Eloquentの機能を活用します
Bad:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Good:
// Model
protected $casts = [
'ordered_at' => 'datetime',
];
public function getSomeDateAttribute($date)
{
return $date->format('m-d');
}
// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
その他 グッドプラクティス
ルートファイルにはロジックを入れない。
Bladeテンプレートの中でVanilla PHPの使用は最小限にする。
割愛箇所
- コミュニティに受け入れられた標準のLaravelツールを使う
- Laravelの命名規則に従う
上記は参画されるプロジェクトの開発ルールに従ってほしいという意図で割愛させていただきました
おわりに
このガイドを参考にして、Laravelでの開発を効率的に進めてみましょう
ベストプラクティスに従うことで、コードの品質が向上し、メンテナンスが容易になります
チーム開発になるとその恩恵をより一層感じるかと思います
また、従うことでどのようなメリットが生まれるのかを自分なりに考えて発信してみるのも良いかもしれませんね