😚

全部入り管理画面、作成・更新処理、クソデカボディパラメータ、何も起きないはずがなく。。。

2022/05/03に公開

こんにちは。tyamahoriです。
PHPエンジニアとして、普段はLaravelを利用してウェブアプリケーションの開発をしています。

ところで、前回の記事、読んでもらえましたか?

https://zenn.dev/tyamahori/articles/98e2c11f64ad48

みなさん!
全部入り管理画面にはどのように向き合っておりますでしょうか?

前回はクソデカクエリに対して、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