私がよく耳にする Laravel に関する質問の 1 つは、「プロジェクトをどのように構築するか」です。 絞り込むと「ロジックがコントローラーにないならどこに置けばいいの?」という部分が一番多いように思います。
問題は、そのような質問に対する唯一の正解がないことです。 Laravel は、構造を自分で選択できる柔軟性を提供します。 Laravel の公式ドキュメントには推奨事項が記載されていないため、特定の例に基づいてさまざまなオプションについて説明します。
知らせ: プロジェクトを構成する方法は 1 つではないため、この記事は補足説明や「もしも」などのパラグラフでいっぱいになります。 それらを飛ばさずに記事全体を読み、ベスト プラクティスのすべての例外に注意することをお勧めします。
多くのことを行うユーザーを登録するための Controller メソッドがあるとします。
public function store(Request $request)
{
// 1. Validation
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', RulesPassword::defaults()],
]);
// 2. Create user
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// 3. Upload the avatar file and update the user
if ($request->hasFile('avatar')) {
$avatar = $request->file('avatar')->store('avatars');
$user->update(['avatar' => $avatar]);
}
// 4. Login
Auth::login($user);
// 5. Generate a personal voucher
$voucher = Voucher::create([
'code' => Str::random(8),
'discount_percent' => 10,
'user_id' => $user->id
]);
// 6. Send that voucher with a welcome email
$user->notify(new NewUserWelcomeNotification($voucher->code));
// 7. Notify administrators about the new user
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($user));
}
return redirect()->route('dashboard');
}
正確には7つのこと。 おそらく、1 つのコントローラー メソッドには多すぎることに同意するでしょう。ロジックを分離し、パーツをどこかに移動する必要があります。 しかし、正確にはどこですか?
- サービス?
- 仕事?
- イベント/リスナー?
- アクションクラス?
- 他の何か?
最も厄介な部分は、上記のすべてが正しい答えになるということです。 それがおそらく、この記事から得られる主なメッセージです。 太字と大文字で強調します。
プロジェクトの構成は自由です。
そこで、言いました。 言い換えれば、どこかで推奨されている構造を見たとしても、それをジャンプしてどこにでも適用しなければならないという意味ではありません。 選択は常にあなた次第です。 後でコードを保守するために、自分自身と将来のチームにとって快適な構造を選択する必要があります。
ということで、記事を今すぐ終わらせることもできるかもしれません。 しかし、あなたはおそらく「肉」が欲しいですよね? わかりました。上のコードをいじってみましょう。
一般的なリファクタリング戦略
まず、「免責事項」です。これにより、ここで何をしているのか、またその理由が明確になります。 私たちの一般的な目標は、Controller メソッドを短くして、ロジックが含まれないようにすることです。
コントローラ メソッドは、次の 3 つのことを行う必要があります。
- ルートまたは他の入力からパラメーターを受け入れる
- いくつかのロジック クラス/メソッドを呼び出し、それらのパラメーターを渡す
- 結果を返す: ビュー、リダイレクト、JSON などを返します。
したがって、コントローラーはメソッドを呼び出しており、コントローラー自体の内部にロジックを実装していません。
また、私が提案した変更は 1 つの方法にすぎないことを覚えておいてください。他にも機能する方法はたくさんあります。 個人的な経験から、私の提案を提供します。
1. 検証: フォーム リクエスト クラス
これは個人的な好みですが、バリデーション ルールを個別に保持するのが好きで、Laravel には優れたソリューションがあります: フォーム リクエスト
したがって、次を生成します。
php artisan make:request StoreUserRequest
検証ルールをコントローラーからそのクラスに移動します。 また、追加する必要があります Password
クラスを上にして変更します authorize()
返却方法 真実:
use IlluminateValidationRulesPassword;
class StoreUserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
];
}
}
最後に、Controller メソッドで、次のように置き換えます。 Request $request
と StoreUserRequest $request
コントローラーから検証ロジックを削除します。
use AppHttpRequestsStoreUserRequest;
class RegisteredUserController extends Controller
{
public function store(StoreUserRequest $request)
{
// No $request->validate needed here
// Create user
$user = User::create([...]) // ...
}
}
OK、コントローラーの最初の短縮が完了しました。 次へ移りましょう。
2. ユーザーの作成: サービスクラス
次に、ユーザーを作成し、そのアバターをアップロードする必要があります。
// Create user
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Avatar upload and update user
if ($request->hasFile('avatar')) {
$avatar = $request->file('avatar')->store('avatars');
$user->update(['avatar' => $avatar]);
}
推奨事項に従う場合、そのロジックはコントローラーに含めるべきではありません。 コントローラーは、ユーザーの DB 構造や、アバターの保存場所について何も認識してはなりません。 すべてを処理するクラス メソッドを呼び出すだけです。
このようなロジックを配置する非常に一般的な場所は、1 つのモデルの操作の周りに個別の PHP クラスを作成することです。 これは Service クラスと呼ばれますが、これは Controller に「サービスを提供する」PHP クラスの「派手な」正式名称です。
そのため、次のようなコマンドはありません php artisan make:service
これは、任意の構造を持つ単なる PHP クラスであるため、IDE 内の任意のフォルダーに手動で作成できます。
通常、Service は次の場合に作成されます。 複数の 同じエンティティまたはモデルの周りのメソッド。 したがって、ここで UserService を作成することにより、ユーザーを作成するためだけでなく、将来ここにさらに多くのメソッドが存在することを想定しています。
また、サービスには通常、次のメソッドがあります。 何かを返す (つまり、「サービスを提供する」)。 対照的に、アクションまたはジョブは通常、何も返されることを期待せずに呼び出されます。
私の場合、私は app/Services/UserService.php
今のところ、メソッドは 1 つです。
namespace AppServices;
use AppModelsUser;
use IlluminateHttpRequest;
use IlluminateSupportFacadesHash;
class UserService
{
public function createUser(Request $request): User
{
// Create user
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Avatar upload and update user
if ($request->hasFile('avatar')) {
$avatar = $request->file('avatar')->store('avatars');
$user->update(['avatar' => $avatar]);
}
return $user;
}
}
次に、コントローラーで、この Service クラスをメソッドのパラメーターとしてタイプヒントし、内部でメソッドを呼び出すことができます。
use AppServicesUserService;
class RegisteredUserController extends Controller
{
public function store(StoreUserRequest $request, UserService $userService)
{
$user = $userService->createUser($request);
// Login and other operations...
はい、電話する必要はありません new UserService()
どこでも。 Laravel では、コントローラーでこのような任意のクラスにタイプヒントを付けることができます。メソッド インジェクションの詳細については、こちらのドキュメントを参照してください。
2.1. 単一責任原則のサービスクラス
現在、コントローラーははるかに短くなっていますが、この単純なコードのコピーと貼り付けの分離には少し問題があります。
最初の問題は、Service メソッドがパラメーターを受け入れるだけで、それらがどこから来たのかを知らない「ブラック ボックス」のように振る舞う必要があることです。 したがって、このメソッドは将来、コントローラ、アーティザン コマンド、またはジョブから呼び出すことができます。
もう 1 つの問題は、Service メソッドが単一責任の原則に違反していることです。つまり、ユーザーを作成してファイルをアップロードします。
そのため、さらに 2 つの「レイヤー」が必要です。1 つはファイルのアップロード用で、もう 1 つはファイルからの変換用です。 $request
関数のパラメーターに。 そして、いつものように、それを実装するにはさまざまな方法があります。
私の場合、ファイルをアップロードする 2 番目のサービス メソッドを作成します。
アプリ/サービス/UserService.php:
class UserService
{
public function uploadAvatar(Request $request): ?string
{
return ($request->hasFile('avatar'))
? $request->file('avatar')->store('avatars')
: NULL;
}
public function createUser(array $userData): User
{
return User::create([
'name' => $userData['name'],
'email' => $userData['email'],
'password' => Hash::make($userData['password']),
'avatar' => $userData['avatar']
]);
}
}
RegisteredUserController.php:
public function store(StoreUserRequest $request, UserService $userService)
{
$avatar = $userService->uploadAvatar($request);
$user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
// ...
繰り返しますが、次のとおりです。 これは物事を分離する 1 つの方法にすぎません。別の方法で行うこともできます。
しかし、私の論理はこれです:
- 方法
createUser()
現在、リクエストについて何も認識していないため、Artisan コマンドまたは他の場所から呼び出すことができます。 - アバターのアップロードは、ユーザー作成操作から分離されています
サービス メソッドは小さすぎて分離できないと思われるかもしれませんが、これは非常に単純化された例です。実際のプロジェクトでは、ファイル アップロード メソッドとユーザー作成ロジックがはるかに複雑になる場合があります。
この場合、「コントローラーを短くする」という神聖なルールから少し離れて、コードの 2 行目を追加しましたが、私の意見では、正当な理由からです。
3. サービスではなくアクション?
近年、アクション クラスの概念が Laravel コミュニティで人気を博しました。 ロジックは次のとおりです。1 つのアクションのみに対して個別のクラスを用意します。 この場合、アクション クラスは次のようになります。
- 新規ユーザーの作成
- ユーザーパスワードの更新
- ユーザープロファイルの更新
- 等
ご覧のとおり、ユーザーに関する同じ複数の操作は、1 つの UserService クラスではなく、Action クラスに分割されています。 単一責任の原則の観点から見ると、それは理にかなっているかもしれませんが、私は多くの個別のクラスを持つのではなく、メソッドをクラスにグループ化するのが好きです。 繰り返しますが、それは個人的な好みです。
それでは、Action クラスの場合にコードがどのようになるかを見てみましょう。
繰り返しますが、ありません php artisan make:action
、PHP クラスを作成するだけです。 たとえば、作成します app/Actions/CreateNewUser.php
:
namespace AppActions;
use AppModelsUser;
use IlluminateHttpRequest;
use IlluminateSupportFacadesHash;
class CreateNewUser
{
public function handle(Request $request)
{
$avatar = ($request->hasFile('avatar'))
? $request->file('avatar')->store('avatars')
: NULL;
return User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'avatar' => $avatar
]);
}
}
Action クラスのメソッド名は自由に選択できます。 handle()
.
RegisteredUserController:
public function store(StoreUserRequest $request, CreateNewUser $createNewUser)
{
$user = $createNewUser->handle($request);
// ...
つまり、すべてのロジックをアクション クラスにオフロードし、アクション クラスがファイルのアップロードとユーザー作成の両方に関するすべてを処理します。 正直なところ、私は個人的に Action クラスの大ファンではなく、あまり使用したことがないので、これが Action クラスを説明するのに最適な例であるかどうかさえわかりません。 例の別のソースとして、Laravel Fortify のコードを見ることができます。
4. バウチャーの作成: 同じサービスですか、それとも異なるサービスですか?
次に、Controller メソッドには、次の 3 つの操作があります。
Auth::login($user);
$voucher = Voucher::create([
'code' => Str::random(8),
'discount_percent' => 10,
'user_id' => $user->id
]);
$user->notify(new NewUserWelcomeNotification($voucher->code));
ログイン操作は、Service と同様に外部クラス Auth を既に呼び出しているため、ここでは変更されません。内部で何が起こっているかを知る必要はありません。
ただし、この場合、バウチャーを使用すると、コントローラーには、バウチャーを作成し、ウェルカム メールと共にユーザーに送信する方法のロジックが含まれています。
まず、バウチャーの作成を別のクラスに移動する必要があります。 VoucherService
同じ内のメソッドとしてそれを置く UserService
. これはほとんど哲学的な議論です。この方法は、バウチャー システム、ユーザー システム、またはその両方にどのような関係があるのでしょうか。
Services の機能の 1 つは複数のメソッドを含むことであるため、1 つのメソッドで “孤独な” VoucherService を作成しないことにしました。 UserService で行います。
use AppModelsVoucher;
use IlluminateSupportStr;
class UserService
{
// public function uploadAvatar() ...
// public function createUser() ...
public function createVoucherForUser(int $userId): string
{
$voucher = Voucher::create([
'code' => Str::random(8),
'discount_percent' => 10,
'user_id' => $userId
]);
return $voucher->code;
}
}
次に、コントローラーでは、次のように呼び出します。
public function store(StoreUserRequest $request, UserService $userService)
{
// ...
Auth::login($user);
$voucherCode = $userService->createVoucherForUser($user->id);
$user->notify(new NewUserWelcomeNotification($voucherCode));
ここで他に考慮すべきことがあります。これらの行の両方を、ウェルカム メールを担当する UserService の別のメソッドに移動し、バウチャー メソッドを呼び出す必要があるのではないでしょうか?
このようなもの:
class UserService
{
public function sendWelcomeEmail(User $user)
{
$voucherCode = $this->createVoucherForUser($user->id);
$user->notify(new NewUserWelcomeNotification($voucherCode));
}
次に、Controller には、このための 1 行のコードしかありません。
$userService->sendWelcomeEmail($user);
5. 管理者への通知: キュー可能なジョブ
最後に、コントローラーに次のコードが表示されます。
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($user));
}
複数のメールを送信する可能性があり、これには時間がかかる可能性があるため、バックグラウンドで実行するためにキューに入れる必要があります。 そこで必要なのがジョブです。
Laravel 通知クラスはキューに入れることができますが、この例では、通知メールを送信するだけでなく、もっと複雑なものがあると想像してみましょう。 それでは、そのためのジョブを作成しましょう。
この場合、Laravel は Artisan コマンドを提供します。
php artisan make:job NewUserNotifyAdminsJob
アプリ/ジョブ/NewUserNotifyAdminsJob.php:
class NewUserNotifyAdminsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private User $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function handle()
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($this->user));
}
}
}
次に、コントローラーで、パラメーターを使用してそのジョブを呼び出す必要があります。
use AppJobsNewUserNotifyAdminsJob;
class RegisteredUserController extends Controller
{
public function store(StoreUserRequest $request, UserService $userService)
{
// ...
NewUserNotifyAdminsJob::dispatch($user);
これで、すべてのロジックを Controller から別の場所に移動しました。
public function store(StoreUserRequest $request, UserService $userService)
{
$avatar = $userService->uploadAvatar($request);
$user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
Auth::login($user);
$userService->sendWelcomeEmail($user);
NewUserNotifyAdminsJob::dispatch($user);
return redirect(RouteServiceProvider::HOME);
}
より短く、さまざまなファイルに分割されていて、それでも読み取り可能ですよね? もう一度繰り返しますが、これはこの使命を達成する唯一の方法であり、別の方法で構築することもできます。
しかし、それだけではありません。 「パッシブ」な方法についても説明しましょう。
6. イベント/リスナー
哲学的に言えば、この Controller メソッドのすべての操作を、アクティブとパッシブの 2 つのタイプに分けることができます。
- そうだった 積極的に ユーザーの作成とログイン
- そして、そのユーザーに関する何かがバックグラウンドで発生する可能性があります (または発生しない可能性があります)。 だから私たちは 受動的に ウェルカム メールの送信と管理者への通知など、その他の操作を待機します。
したがって、コードを分離する 1 つの方法として、コントローラー内で呼び出すのではなく、何らかのイベントが発生したときに自動的に起動する必要があります。
イベントとリスナーの組み合わせを使用できます。
php artisan make:event NewUserRegistered
php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered
php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered
イベント クラスは、そのイベントの任意のリスナーに渡される User モデルを受け入れる必要があります。
アプリ/イベント/NewUserRegistered.php
use AppModelsUser;
class NewUserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
次に、次のように、イベントがコントローラーからディスパッチされます。
public function store(StoreUserRequest $request, UserService $userService)
{
$avatar = $userService->uploadAvatar($request);
$user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
Auth::login($user);
NewUserRegistered::dispatch($user);
return redirect(RouteServiceProvider::HOME);
}
そして、Listener クラスでは、同じロジックを繰り返します。
use AppEventsNewUserRegistered;
use AppServicesUserService;
class NewUserWelcomeEmailListener
{
public function handle(NewUserRegistered $event, UserService $userService)
{
$userService->sendWelcomeEmail($event->user);
}
}
そして、もう一つ:
use AppEventsNewUserRegistered;
use AppNotificationsNewUserAdminNotification;
use IlluminateSupportFacadesNotification;
class NewUserNotifyAdminsListener
{
public function handle(NewUserRegistered $event)
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($event->user));
}
}
}
イベントとリスナーを使用したこのアプローチの利点は何ですか? それらはコード内で「フック」のように使用され、将来、他の誰もがそのフックを使用できるようになります。 つまり、将来の開発者に対して、「ユーザーが登録されました。イベントが発生しました。ここで発生している他の操作を追加したい場合は、リスナーを作成してください」と言っているのです。
7. オブザーバー: 「サイレント」イベント/リスナー
この場合、モデル オブザーバーを使用して、非常によく似た「パッシブ」アプローチを実装することもできます。
php artisan make:observer UserObserver --model=User
アプリ/オブザーバー/UserObserver.php:
use AppModelsUser;
use AppNotificationsNewUserAdminNotification;
use AppServicesUserService;
use IlluminateSupportFacadesNotification;
class UserObserver
{
public function created(User $user, UserService $userService)
{
$userService->sendWelcomeEmail($event->user);
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($event->user));
}
}
}
その場合、コントローラーでイベントをディスパッチする必要はありません。オブザーバーは、Eloquent モデルが作成された直後に起動されます。
便利ですよね?
ただ、個人的な意見としては、これは少し危険なパターンです。 実装ロジックがコントローラから隠されているだけでなく、マザー 存在 それらの操作の明確ではありません。 1 年後にチームに加わった新しい開発者を想像してみてください。彼らは、ユーザー登録を維持するときに、考えられるすべてのオブザーバー メソッドをチェックしますか?
もちろん、それを理解することは可能ですが、それでも明らかではありません。 そして、私たちの目標は、コードをより保守しやすいものにすることです。そのため、「サプライズ」が少ないほど良いのです。 だから、私はオブザーバーの大ファンではありません。
結論
今この記事を読んでいると、非常に単純な例で、コードの可能な分離の表面をなぞったにすぎないことがわかります。
実際、この単純な例では、1 つではなく、さらに多くの PHP クラスを作成して、アプリケーションをより複雑にしているように見えるかもしれません。
ただし、この例では、これらの個別のコード部分は短いです。 実際には、それらははるかに複雑である可能性があり、それらを分離することで管理しやすくなったため、たとえば、すべての部分を別の開発者が処理できます。
一般的に、最後に繰り返します。アプリケーションを担当するのはあなたであり、コードを配置する場所を決定するのはあなただけです。 目標は、あなたやあなたのチームメイトが将来それを理解し、新しい機能を追加したり、既存のものを維持/修正したりするのに問題がないようにすることです.