Laravel+Inertia.jsでパラメータのprops化の仕組みを調べた

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


Laravel Jetstreamのパッケージを利用すると、LivewireやInertiaのパッケージを一緒に導入することが出来ます。
今回はInertia+Vueの使い方を見ていきます。

## inertia
inertia.js自体はLaravel独自のJavaScriptアダプターというわけではなく、サーバーサイドはLaravel，Rails、フロントはVue，React、Sveltなどの選択肢が存在しています。MVCフレームワークでなるべく簡単にモダンJSを導入出来ることに価値があるようです。

https://inertiajs.com/server-side-setup

Jetstreamでの導入

https://jetstream.laravel.com/2.x/installation.html


## 使い方

Laravelに導入した場合、bladeを呼ぶようにルートのURLとパラメータを`Inertia::render`に渡します

```php
use Illuminate\Http\Request;
use Inertia\Inertia;

/**
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Inertia\Response
 */
public function show(Request $request)
{
    return Inertia::render('index', [
        'message' => 'test',
    ]);
}
```

Vueファイル側ではpropsでパラメータを受けることが出来ます。

```index.vue
<script setup >
const props = defineProps<{
    message: string;
}>();
</script>

<template>
{{ props.message }}
</template>
```

この仕組みを使って、認証やルーティングをサーバーサイドのままに、フロントはVueやReactファイルを使って実装出来ます。

## props化の仕組みは？
さてパラメータのprops化はどこでやってるのでしょうか...?

inertia.jsとLaravelの間に入るのは`inertia-laravel`というアダプター用のコードが用意されています。


https://github.com/inertiajs/inertia-laravel

何か結構大変なことやってそう...？

Laravel側で呼んでる`Inertia::render`はFacade定義された`Inertia`を使っています。

```Inertia.php

<?php

namespace Inertia;

use Illuminate\Support\Facades\Facade;

/**
 * @method static void setRootView(string $name)
 * @method static void share($key, $value = null)
 * @method static array getShared(string $key = null, $default = null)
 * @method static array flushShared()
 * @method static void version($version)
 * @method static int|string getVersion()
 * @method static LazyProp lazy(callable $callback)
 * @method static Response render($component, array $props = [])
 * @method static \Illuminate\Http\Response location(string $url)
 *
 * @see \Inertia\ResponseFactory
 */
class Inertia extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return ResponseFactory::class;
    }
}
```
実態は`ResponseFactoryクラス`のようです。

ResponseFactoryの`render`は

```ResponseFactory.php
    /**
     * @param  string  $component
     * @param  array|Arrayable  $props
     * @return Response
     */
    public function render(string $component, $props = []): Response
    {
        if ($props instanceof Arrayable) {
            $props = $props->toArray();
        }

        return new Response(
            $component,
            array_merge($this->sharedProps, $props),
            $this->rootView,
            $this->getVersion()
        );
    }

```
引数で受ける`$component`がURLの文字列、`$props`がパラメータ配列ですね。
それを利用してResponseクラスのインスタンスを生成しています。

ここのResponseクラスは`inertia-laravel`の中にあるクラスです。

https://github.com/inertiajs/inertia-laravel/blob/master/src/Response.php

このクラスは`Illuminate\Contracts\Support\Responsable`という`interface`を継承することで`toResponse`メソッドを持つHTTPレスポンスクラスとして使えます。

https://github.com/illuminate/contracts/blob/master/Support/Responsable.php

```Response.php
    /**
     * @param  string  $component
     * @param  array|Arrayable  $props
     * @param  string  $rootView
     * @param  string  $version
     */
    public function __construct(string $component, $props, string $rootView = 'app', string $version = '')
    {
        $this->component = $component;
        $this->props = $props instanceof Arrayable ? $props->toArray() : $props;
        $this->rootView = $rootView;
        $this->version = $version;
    }
```
コンストラクタで`class変数`に引数を保存して、元の`Controller`の`return`に返ります。

そこからLaravelのコード内で`vendor/laravel/framework/src/Illuminate/Routing/Router.php`の`toResponse`メソッドでレスポンスクラスの`toResponse`が呼ばれます

```Router.php
    /**
     * Static version of prepareResponse.
     *
     * @param  \Symfony\Component\HttpFoundation\Request  $request
     * @param  mixed  $response
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public static function toResponse($request, $response)
    {
        if ($response instanceof Responsable) {
            $response = $response->toResponse($request);
        }

        if ($response instanceof PsrResponseInterface) {
            $response = (new HttpFoundationFactory)->createResponse($response);
        } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
            $response = new JsonResponse($response, 201);
        } elseif ($response instanceof Stringable) {
            $response = new Response($response->__toString(), 200, ['Content-Type' => 'text/html']);
        } elseif (! $response instanceof SymfonyResponse &&
                   ($response instanceof Arrayable ||
                    $response instanceof Jsonable ||
                    $response instanceof ArrayObject ||
                    $response instanceof JsonSerializable ||
                    $response instanceof stdClass ||
                    is_array($response))) {
            $response = new JsonResponse($response);
        } elseif (! $response instanceof SymfonyResponse) {
            $response = new Response($response, 200, ['Content-Type' => 'text/html']);
        }

        if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
            $response->setNotModified();
        }

        return $response->prepare($request);
    }
```

https://github.com/illuminate/routing/blob/be989aa339a1baaa2d1bff373a6fb285dc4ef5ad/Router.php#L885


`Responsable`のinstanceであれば`toResponse`メソッドが呼ばれています。


`inertia-laravel`の中にある`Response`クラスです。
```Response.php

...

use Illuminate\Support\Facades\Response as ResponseFactory;

...

    /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function toResponse($request)
    {
        $only = array_filter(explode(',', $request->header('X-Inertia-Partial-Data', '')));

        $props = ($only && $request->header('X-Inertia-Partial-Component') === $this->component)
            ? Arr::only($this->props, $only)
            : array_filter($this->props, static function ($prop) {
                return ! ($prop instanceof LazyProp);
            });

        $props = $this->resolvePropertyInstances($props, $request);

        $page = [
            'component' => $this->component,
            'props' => $props,
            'url' => $request->getBaseUrl().$request->getRequestUri(),
            'version' => $this->version,
        ];

        if ($request->header('X-Inertia')) {
            return new JsonResponse($page, 200, ['X-Inertia' => 'true']);
        }

        return ResponseFactory::view($this->rootView, $this->viewData + ['page' => $page]);
    }
```


https://github.com/inertiajs/inertia-laravel/blob/b00fe8cf741766b41de69978f9e90ee6d1470d36/src/Response.php#L87

最終的には`$page`に情報を詰めて既に`X-Inertia`がtrueの場合`JsonResponse`を返す。
そうでない場合`Facade`の`Response`で`viewメソッド`を呼びます。

`X-Inertia`にまつわる挙動に関してはこちらの記事がとても詳しく参考になります。

https://qiita.com/okok-97/items/5798e4a60f32b653722d

`Facade`の`Response`の`view`に行った後、makeメソッドでviewを通したresponseを作っています。
```routing/Response.php

    /**
     * Create a new response for a given view.
     *
     * @param  string|array  $view
     * @param  array  $data
     * @param  int  $status
     * @param  array  $headers
     * @return \Illuminate\Http\Response
     */
    public function view($view, $data = [], $status = 200, array $headers = [])
    {
        if (is_array($view)) {
            return $this->make($this->view->first($view, $data), $status, $headers);
        }

        return $this->make($this->view->make($view, $data), $status, $headers);
    }
```

https://github.com/illuminate/routing/blob/037311f85805e91ef085663d894fa9c003e0b6e6/ResponseFactory.php#L81

という感じでこの先は通常の`view`の処理になっていきます。
渡したパラメータはアダプター内の変換で`props`を`key名`にしてテンプレート側へ渡っていきました。


さらにfront側でどう受け取って扱っていくかは今度はjs側のアダプターコードを読んでいくことで分かりそうです。
これはまた別の機会に読んでみようと思います。

https://github.com/inertiajs/inertia