laravel-best-practices
単一責任の原則
クラスとメソッドは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:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
...
}
Good:
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つの責任だけを持たないといけません、そのためビジネスロジックはコントローラからサービスクラスに移動させます。
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');
}
}
}
繰り返し書かない (DRY)
可能であればコードを再利用します。単一責任の原則は重複を避けることに役立ちます。また、Bladeテンプレートを再利用したり、Eloquentのスコープなどを使用したりします。
Bad:
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により読みやすくメンテナンスしやすいコードを書くことができます。また、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();
マスアサインメント
Bad:
$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();
Good:
$category->article()->create($request->validated());
Bladeテンプレート内でクエリを実行しない。Eager Lodingを使う(N + 1問題)
Bad (100ユーザに対して、101回のDBクエリが実行される):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Good (100ユーザに対して、2回のDBクエリが実行される):
$users = User::with('profile')->get();
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
コメントを書く。ただしコメントよりも説明的なメソッド名と変数名を付けるほうが良い
Bad:
if (count((array) $builder->getQuery()->joins) > 0)
Better:
// Determine if there are any joins.
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();
もっとも良い方法は、データを転送するためJSパッケージに特別なPHPを使用することです。
コード内の文字列の代わりに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ツールを使う
サードパーティ製のパッケージやツールの代わりに、Laravel標準機能とコミュニティパッケージを使うことを推奨します。将来あなたと共に働くことになるどの開発者も新しいツールを学習する必要があります。また、サードパーティ製のパッケージやツールを使用している場合は、Laravelコミュニティから助けを得る機会が大幅に少なくなります。あなたのクライアントにその代金を払わせないでください。
タスク | 標準ツール | サードパーティ製ツール |
---|---|---|
認可 | Policies | Entrust, Sentinel または他のパッケージ |
アセットコンパイル | Laravel Mix, Vite | Grunt, Gulp, サードパーティ製パッケージ |
開発環境 | Laravel Sail, Homestead | Docker |
デプロイ | Laravel Forge | Deployer またはその他ソリューション |
単体テスト | PHPUnit, Mockery | Phpspec, Pest |
ブラウザテスト | Laravel Dusk | Codeception |
DB | Eloquent | SQL, Doctrine |
テンプレート | Blade | Twig |
データの取り扱い | Laravel collections | Arrays |
フォームバリデーション | Request classes | サードパーティ製パッケージ、コントローラ内でバリデーション |
認証 | 標準組み込み | サードパーティ製パッケージ、独自実装 |
API 認証 | Laravel Passport, Laravel Sanctum | サードパーティ製の JWT や OAuth パッケージ |
API作成 | 標準組み込み | Dingo API や類似パッケージ |
DB構造の取り扱い | Migrations | 直接DB構造を扱う |
ローカライゼーション | 標準組み込み | サードパーティ製パッケージ |
リアルタイムユーザインターフェース | Laravel Echo, Pusher | サードパーティ製パッケージ または直接Webソケットを扱う |
テストデータ生成 | Seeder classes, Model Factories, Faker | 手動でテストデータを作成 |
タスクスケジューリング | Laravel Task Scheduler | スクリプトやサードパーティ製パッケージ |
DB | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
Laravelの命名規則に従う
PSRに従います。
また、Laravelコミュニティに受け入れられた命名規則に従います。
対象 | 規則 | Good | Bad |
---|---|---|---|
コントローラ | 単数形 | ArticleController | |
ルート | 複数形 | articles/1 | |
名前付きルート | スネークケースとドット表記 | users.show_active | |
モデル | 単数形 | User | |
hasOne または belongsTo 関係 | 単数形 | articleComment | |
その他すべての関係 | 複数形 | articleComments | |
テーブル | 複数形 | article_comments | |
Pivotテーブル | 単数形 モデル名のアルファベット順 | article_user | |
テーブルカラム | スネークケース モデル名は含めない | meta_title | |
モデルプロパティ | スネークケース | $model->created_at | |
外部キー | 単数形 モデル名の最後に_idをつける | article_id | |
主キー | - | id | |
マイグレーション | - | 2017_01_01_000000_create_articles_table | |
メソッド | キャメルケース | getAll | |
リソースコントローラのメソッド | 一覧 | store | |
テストクラスのメソッド | キャメルケース | testGuestCannotSeeArticle | |
変数 | キャメルケース | $articlesWithAuthor | |
コレクション | 説明的、 複数形 | $activeUsers = User::active()->get() | |
オブジェクト | 説明的, 単数形 | $activeUser = User::active()->first() | |
設定ファイルと言語ファイルのインデックス | スネークケース | articles_enabled | |
ビュー | ケバブケース | show-filtered.blade.php | |
コンフィグ | スネークケース | google_calendar.php | |
契約 (インターフェイス) | 形容詞または名詞 | AuthenticationInterface | |
Trait | 形容詞 | Notifiable | |
Trait (PSR) | adjective | NotifiableTrait | |
Enum | singular | UserType |
|
FormRequest | singular | UpdateUserRequest |
|
Seeder | singular | UserSeeder |
できるだけ短く読みやすい構文で書く
Bad:
$request->session()->get('cart');
$request->input('name');
Good:
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の代わりにIoCコンテナもしくはファサードを使う
new構文はクラス間の密結合を生み出し、テストすることを難しくします。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ファイルへデータを渡します。そして、アプリケーション内でデータを参照する場合はconfig()
ヘルパー関数を使います。
Bad:
$apiKey = env('API_KEY');
Good:
// config/api.php
'key' => env('API_KEY'),
// データを使用する
$apiKey = config('api.key');
日付を標準フォーマットで保存する。アクセサとミューテータを使って日付フォーマットを変更する
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(標準のPHPコードを記述すること)の使用は最小限にします。
元記事
Discussion