Laravel ファサード、ヘルパーを使わないコントローラを作る

[Laravel Advent Calendar 2022](https://qiita.com/advent-calendar/2022/laravel) 19日目の記事です。

:::note info
ファサードやヘルパーをガッツリ使った方が実装速度は早いので通常の開発では採用しなくて良いと思います。
:::

## ファサードを利用しないメリット

- クラスの依存関係がわかりやすくなる
- テストがしやすくなる
- 再利用性が高くなる

### 参考記事

https://blog.ytake.jp.net/entry/2015/12/16/011812

https://www.slideshare.net/KenjiroKubota/laravelfacade

https://www.slideshare.net/KenjiroKubota/laraveltips

https://qiita.com/mikankari/items/40627769f8487853bc5f

## Laravelのベースコントローラについて

Laravelフレームワークが用意している `app/Http/Controllers/Controller.php` を親クラスとして `make:controller` でコントローラを作っていきます。

```php:app/Http/Controllers/Controller.php
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
```

- AuthorizesRequests 認可処理はフォームリクエストで行うからいらない
- DispatchesJobs キューを多くのコントローラで使わない。使うコントローラだけトレイト使えばいいのでは？
- ValidatesRequests 検証処理はフォームリクエストで行うからいらない

[App\Http\Controllers\Controller](https://github.com/laravel/laravel/blob/9.x/app/Http/Controllers/Controller.php) は [Illuminate\Routing\Controller](https://github.com/laravel/framework/blob/9.x/src/Illuminate/Routing/Controller.php) を継承してます。

```php:vendor/laravel/framework/src/Illuminate/Routing/Controller.php
<?php

namespace Illuminate\Routing;

use BadMethodCallException;

abstract class Controller
{
    /**
     * The middleware registered on the controller.
     *
     * @var array
     */
    protected $middleware = [];

    /**
     * Register middleware on the controller.
     *
     * @param  \Closure|array|string  $middleware
     * @param  array  $options
     * @return \Illuminate\Routing\ControllerMiddlewareOptions
     */
    public function middleware($middleware, array $options = [])
    {
        foreach ((array) $middleware as $m) {
            $this->middleware[] = [
                'middleware' => $m,
                'options' => &$options,
            ];
        }

        return new ControllerMiddlewareOptions($options);
    }

    /**
     * Get the middleware assigned to the controller.
     *
     * @return array
     */
    public function getMiddleware()
    {
        return $this->middleware;
    }

    /**
     * Execute an action on the controller.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function callAction($method, $parameters)
    {
        return $this->{$method}(...array_values($parameters));
    }

    /**
     * Handle calls to missing methods on the controller.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public function __call($method, $parameters)
    {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }
}
```

コントローラでミドルウェアを実行したくないし、callActionもなくても大丈夫です。

### 参考

https://zenn.dev/shlia/scraps/bb85c4eaeb7f02

## Laravel ファサード、ヘルパーを使わないコントローラを作る

元々用意されている `app/Http/Controllers/Controller.php` ファイルを上書きして作成します。

```php:app/Http/Controllers/Controller.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Session\Session;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Routing\Redirector;
use Illuminate\Routing\ResponseFactory;

abstract class Controller
{
    protected const SESSION_SUCCESS = 'success';

    protected const SESSION_FAILURE = 'failure';

    protected readonly StatefulGuard $auth;

    public function __construct(
        protected readonly LoggerInterface $logger,
        protected readonly Session $session,
        protected readonly Translator $translator,
        protected readonly ViewFactory $viewFactory,
        protected readonly ResponseFactory $responseFactory,
        protected readonly Redirector $redirector,
        AuthFactory $authFactory,
    ) {
        $this->auth = $authFactory->guard('web');
    }
}
```

利用したいインスタンスのみコンストラクタインジェクションで取得してます。
クラスの依存関係がわかりやすくなり、Laravelに明るくない人にも優しいと思います。

ロガー、セッション、翻訳、ビュー、レスポンス、リダイレクトレスポンス、認証はよく利用すると思うので例として挙げました。

`Illuminate\Contracts\Auth\StatefulGuard` についてはこのインターフェースはコンストラクタインジェクションで注入してくれません。
サービスプロバイダーで注入させても良いですが、システムによっては複数の認証方法があるかもしれないのでベースコントローラで選択するのが良さそうです。

## Laravel ファサード、ヘルパーを使わないコントローラの使い方

### ログインのコントローラ

```php:app/Http/Controllers/LoginController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Request\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;

final class LoginController extends Controller
{
    public function __invoke(LoginRequest $request): RedirectResponse
    {
        $credentials = [
            'email' => (string) $request->input('email'),
            'password' => (string) $request->input('password'),
        ];

        if ($this->auth->attempt($credentials, (bool) $request->input('remember'))) {
            $this->session->regenerate();

            return $this->redirector->route('dashboard');
        }

        return $this->redirector->back();
    }
}
```

### ユーザー登録のコントローラ

```php:app/Http/Controllers/SignUpController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Request\Register\SignUpRequest;
use Illuminate\Http\RedirectResponse;
use Throwable;
use Todo\Application\Auth\SignUpUseCase;
use Todo\Application\Auth\SignUpUseCaseInput;

final class SignUpController extends Controller
{
    public function __invoke(SignUpRequest $request, SignUpUseCase $useCase): RedirectResponse
    {
        $input = new SignUpUseCaseInput(
            (string) $request->input('name'),
            (string) $request->input('email'),
            (string) $request->input('password'),
        );

        try {
            $output = $useCase->signUp($input);
        } catch (Throwable $exception) {
            return $this->redirector
                ->back()
                ->withErrors([self::SESSION_FAILURE => $exception->getMessage()]);
        }

        $this->auth->loginUsingId($output->userId);
        $this->session->regenerateToken();
        $this->session->put(self::SESSION_SUCCESS, $this->translator->get('app.signUp.completed'));
        $this->logger->info('Completed sign up process. userId: ' . $output->userId);

        return $this->redirector->route('dashboard');
    }
}
```

`SignUpRequest`や`SignUpUseCase` など個別のアクションで利用するクラスはメソッドインジェクションを利用してインスタンスを取得します。

## さいごに

コピペで使えるものが欲しかったので参考例として作っておきます。
必要に応じて追加、削除してご利用ください。

ファサードやヘルパーを無効化する手順は参考記事やスライドを参照ください。