全部入り管理画面、作成・更新処理、クソデカボディパラメータ、何も起きないはずがなく。。。
こんにちは。tyamahoriです。
PHPエンジニアとして、普段はLaravelを利用してウェブアプリケーションの開発をしています。
ところで、前回の記事、読んでもらえましたか?
みなさん!
全部入り管理画面にはどのように向き合っておりますでしょうか?
前回はクソデカクエリに対して、DTOをどうやって使っていくかの記事を書きました。今回は全部入り管理画面にてデータを作成・更新する際のノウハウを共有したいと思います。
よくありそうな実装
ユーザーを作成する処理があったとして、そのコードサンプルを作ってみました。
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
/**
* 雰囲気コードです。
*/
class HogeController extends Controller
{
/**
* @param Request $request
* @param Hasher $hasher
* @param ResponseFactory $response
* @param User $user
*/
public function __construct(
private readonly Request $request,
private readonly Hasher $hasher,
private readonly ResponseFactory $response,
private readonly User $user
) {
$request->validate([
'email' => ['required', 'string'],
'password' => ['required', 'string'],
]);
}
/**
* @return Response
*/
public function __invoke(): Response
{
$this->user
->save([
'email' => $this->request->input('email'),
'password' => $this->hasher->make($this->request->input('password')),
]);
return $this->response->noContent();
}
}
invokeメソッドを利用したシングルアクションコントローラーとしています。バリデーションはconstruct内部で行うほうが良いのではと思っています。こうすればinvokeメソッド内部では、バリデーション済みの値を前提として、処理を実装できるかなと思っています。これでも十分な気がしますが、疑問点を上げるとすれば、、、
- コントローラーがDBカラムのことを知ってしまっている
- validation処理は別クラスに切り出してもよい(?)
というところでしょうか。。。本当はユースケース(アプリケーションサービス)クラスも作りたいところですが、今回は妥協して作らないことにします。
FormRequestから距離をおこう!
FormRequestの存在を否定していません。とても便利です。しかしながら考えもなしにFormRequestを使うのは良くないと考えています。。。!(ここで自分にブーメランが突き刺さる)
そのFormRequest、 Requestクラスでも良くないですか?
をキーフレーズとして、立ち返ってみましょう。
そこで、今回は入力値を取得するDTOクラスを作ります。Interfaceを利用します。
<?php
namespace App\Package;
interface CreateUserInputInterface
{
/**
* @return string
*/
public function email(): string;
/**
* @return string
*/
public function password(): string;
}
僕たちがほしいのはFormRequestではなく、バリデートされた値だと思います。。!そして具象クラスを作ります。
<?php
namespace App\Package;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Http\Request;
class CreateUserInputRequest implements CreateUserInputInterface
{
/**
* @param Request $request
* @param Hasher $hasher
*/
public function __construct(
private readonly Request $request,
private readonly Hasher $hasher,
) {
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string', 'min:8'],
]);
}
/**
* @return string
*/
public function email(): string
{
return $this->request->input('email');
}
/**
* @return string
*/
public function password(): string
{
// Hash対応を忘れない!本当は命名やどこでハッシュ対応するかは考えるべき
return $this->hasher->make($this->request->input('password'));
}
}
LaravelのRequestクラスをDIしてバリデーションを行っています。
副作用が発生する処理はRepostioryで!
そして俺達のUserRepostioryを作ります。
<?php
namespace App\Package;
interface UserRepositoryInterface
{
/**
* @param CreateUserInputInterface $input
* @return void
*/
public function createByInput(CreateUserInputInterface $input): void;
}
からの具象クラスです。
<?php
namespace App\Package;
use App\Models\User;
class UserRepositoryEloquent implements UserRepositoryInterface
{
/**
* @param User $user
*/
public function __construct(
private readonly User $user
) {
}
/**
* @param CreateUserInputInterface $input
* @return void
*/
public function createByInput(CreateUserInputInterface $input): void
{
$this->user->save([
'email' => $input->email(),
'password' => $input->password(),
]);
}
}
ポイントとしては、User Eloquentを外側から使っているということです。Eloquent内部に定義することもできますが、他の場面で使われてしまうと色々と厄介なので。。。!
注意点としては、Interfaceを作っているので、AppServiceProviderあたりでbindの対応をおこないましょう!今回は割愛します。
改善後のコード例
<?php
namespace App\Http\Controllers;
use App\Package\CreateUserInputInterface;
use App\Package\UserRepositoryInterface;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;
/**
* 雰囲気コードです。
*/
class HogeController extends Controller
{
/**
* @param CreateUserInputInterface $userInput
* @param UserRepositoryInterface $userRepository
* @param ResponseFactory $response
*/
public function __construct(
private readonly CreateUserInputInterface $userInput,
private readonly UserRepositoryInterface $userRepository,
private readonly ResponseFactory $response
) {
}
/**
* @return Response
*/
public function __invoke(): Response
{
$this->userRepository->createByInput($this->userInput);
return $this->response->noContent();
}
}
まとめ
責務の役割が明確化になり、コードが見やすくなったかなと思いますが、いかがでしょう?
- CreateUserInputInterface -> 入力値をバリデーションして、DTOに変換する
- UserRepositoryInterface -> DTOを受け取り、永続化を行う
- ResponseFactory -> クライアントへのレスポンス生成
今後、入力値の増減においては、CreateUserInputInterfaceにてgetterメソッドを追加します。UserRepositoryInterfaceでは、CreateUserInputInterfaceにて追加されたgetterメソッドを利用してデータの生成、更新ができるかなと思います。
色々と突っ込みどころはあるかもしれません。状況によっては、全部入り管理画面のデータ作成、更新処理はコントローラーにすべて詰め込むのものありだとは思います。
それでは良きLaravel開発者ライフを〜!
Discussion