個人ブログ

インフラ、アプリケーションまわり

LaravelのRequestオブジェクトの動きを追う

PHPフレームワークのLaravelでは、Requestオブジェクトを通じて、リクエスト情報を簡単に取得できる。

PHPならばスーパーグローバル変数である$_POST[$key]とかで逐一取得するところも、LaravelだとRequestオブジェクトのインスタンス$requestから、$request->input($key)とかで取得できる。

この流れについて、Laravel内部ではどのような動きをしているのか、ということについて追ってみる。

Requestオブジェクト取得

ここでは、フォームから送信された値を取得するケースを考える。

PHPだと、フォームから送信されたvalue値をそれぞれ$_POST[$key]で取得するケースである。

フォームからの送信値が、name属性としてnameというkey名を持っていたとすると、Laravelでは下記のような記述で取得することができる。

    public function store(ValidateRequest $request)
    {
        inputName = $request->input('name');
    }

今回は、フォームリクエスValidateRequestから値を取得している。インスタンス化に際しては、サービスコンテナで自動的に解決される。

フォームリクエストの記述を追う

フォームリクエストは、Illuminate\Foundation\Http\FormRequestを継承しているので、次にこちらのソースコードを追ってみる。

$request->input()で値を取得しているので、input()メソッドが存在しないか探してみるが、ここには存在していない。

Illuminate\Foundation\Http\FormRequestは、Illuminate\Http\Requestを継承しているので、次にこちらのソースコードを追ってみる。

Illuminate\Http\Requestを追う

しかし、このファイル内にもinput()が存在しない、、

しかし、Requestクラス内で、Concerns\InteractsWithInputtraitをuseしており、名前的にここにありそうだと踏む。

そこで次に、Concerns\InteractsWithInputソースコードを追ってみる。

Concerns\InteractsWithInputを追う。

同ファイルの206行目、ついにそれらしき記述を発見した。

    public function input($key = null, $default = null)
    {
        return data_get(
            $this->getInputSource()->all() + $this->query->all(), $key, $default
        );
    }

上記メソッドの説明を見ると、Retrieve an input item from the request.とある。中身を見ると、data_get()を用いているが、これはLaravelのヘルパ関数である。

readouble.com

    function data_get($target, $key, $default = null)
    {
        if (is_null($key)) {
            return $target;
        }

        $key = is_array($key) ? $key : explode('.', $key);

        while (! is_null($segment = array_shift($key))) {
            if ($segment === '*') {
                if ($target instanceof Collection) {
                    $target = $target->all();
                } elseif (! is_array($target)) {
                    return value($default);
                }

                $result = [];

                foreach ($target as $item) {
                    $result[] = data_get($item, $key);
                }

                return in_array('*', $key) ? Arr::collapse($result) : $result;
            }

            if (Arr::accessible($target) && Arr::exists($target, $segment)) {
                $target = $target[$segment];
            } elseif (is_object($target) && isset($target->{$segment})) {
                $target = $target->{$segment};
            } else {
                return value($default);
            }
        }

        return $target;
    }

input()の中身をより詳しくみると、$this->getInputSource()->all() + $this->query->all()の部分は対象の配列であり、配列に存在する$key名のvaluw値を返却している。

$key名として渡されるのは、ここではフォームのname属性である。ということは、$this->getInputSource()->all() + $this->query->all()の部分に、$_POSTのような連想配列が渡されていると考えられる。

そこで、次にこの部分を追ってみる。

$this->getInputSource()->all() + $this->query->all()を追う

まずは、getInputSource()が何かを追う。おそらくこの部分で、配列の実体が返却されることが想定される。getInputSource()がどこにあるのかというと、Illuminate/Http/Requestに定義されている。

    protected function getInputSource()
    {
        if ($this->isJson()) {
            return $this->json();
        }

        return in_array($this->getRealMethod(), ['GET', 'HEAD']) ? $this->query : $this->request;
    }

説明をみると、Get the input source for the request.とある。リクエスト情報を取得していることが予想できる。

関数の中身を詳しくみてみる。内部は3項演算子で構成されていて、条件式はin_array($this->getRealMethod(), ['GET', 'HEAD'])である。決め打ちになってしまうが、HTTPリクエストのメソッドを取得しているものだと考えられる。そして、HTTPメソッドがGET, HEADのどちらかであれば、$this->queryを返却し、それ以外であれば$this->requestを返却していることが分かる。

今回の場合はPOSTメソッドなので、$this->requestが配列の実体だろうと考えられる。

$this->requestを追う

$this->requestだが、クラスプロパティとしてはSymfony\Component\HttpFoundation\Requestに定義されているので、こちらを追ってみる。

    /**
     * Request body parameters ($_POST).
     *
     * @var \Symfony\Component\HttpFoundation\ParameterBag
     */
    public $request;

上記が、$requestのプロパティに該当する。$_POSTのパラメータであることも明記されており、上記で扱っていることが分かる。

中身がどこで渡されているのかだが、まずは__construct()に着目する。

    /**
     * @param array                $query      The GET parameters
     * @param array                $request    The POST parameters
     * @param array                $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
     * @param array                $cookies    The COOKIE parameters
     * @param array                $files      The FILES parameters
     * @param array                $server     The SERVER parameters
     * @param string|resource|null $content    The raw body data
     */
    public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
    {
        $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
    }

配列の引数として、$requestが渡されているが、実際はinitialize()の引数に渡っている。

    /**
     * Sets the parameters for this request.
     *
     * This method also re-initializes all properties.
     *
     * @param array                $query      The GET parameters
     * @param array                $request    The POST parameters
     * @param array                $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
     * @param array                $cookies    The COOKIE parameters
     * @param array                $files      The FILES parameters
     * @param array                $server     The SERVER parameters
     * @param string|resource|null $content    The raw body data
     */
    public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
    {
        $this->request = new ParameterBag($request);
        $this->query = new ParameterBag($query);
        $this->attributes = new ParameterBag($attributes);
        $this->cookies = new ParameterBag($cookies);
        $this->files = new FileBag($files);
        $this->server = new ServerBag($server);
        $this->headers = new HeaderBag($this->server->getHeaders());

        $this->content = $content;
        $this->languages = null;
        $this->charsets = null;
        $this->encodings = null;
        $this->acceptableContentTypes = null;
        $this->pathInfo = null;
        $this->requestUri = null;
        $this->baseUrl = null;
        $this->basePath = null;
        $this->method = null;
        $this->format = null;
    }

上記関数の$this->request = new ParameterBag($request);にて、中身が渡されているようだ。左記でインスタンス化されている\Symfony\Component\HttpFoundation\ParameterBagは、パラメータを総合的に扱っているファイルのように見受けられる。

ライフサイクル的アプローチにより、リクエストを探る

次に観点を変えて、Laravelのライフサイクル的アプローチによって、どこでリクエスト情報が取得されているのかを調査する。

Laravelのライフサイクルだが、周知の通り、エントリポイントであるpublic/index.phpが処理のすべての入り口である。

<?php

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

上記が、public/index.phpの中身である。(コメントは省略してある。)

この中で、リクエスト情報に関連するのは4行目(空欄を含めると9行目)以降である。

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

上記の、Illuminate\Http\Request::capture()によってRequestが生成され、引数としてhandle()に渡されている。

handle()によって引数のリクエスト情報がルーティングに渡され、リクエスト情報に含まれるパスが判定され、定義したコントローラーのメソッドに処理が移るという流れである。

ということで、Illuminate\Http\Request::capture()でリクエスト情報を取得している実装を追ってみる。

Illuminate\Http\Request::capture()を追う

実際にIlluminate\Http\Requestに定義されたcapture()ソースコードを見ると、下記のような記述である。

    /**
     * Create a new Illuminate HTTP request from server variables.
     *
     * @return static
     */
    public static function capture()
    {
        static::enableHttpMethodParameterOverride();

        return static::createFromBase(SymfonyRequest::createFromGlobals());
    }

return static::createFromBase(SymfonyRequest::createFromGlobals());だが、引数にもメソッドcreateFromGlobals()が呼ばれている。

まずは、上記メソッドを追ってみる。

createFromGlobals()を追う

createFromGlobals()は、Symfony\Component\HttpFoundation\Requestに静的メソッドとして、下記のように定義されている。

    /**
     * Creates a new request with values from PHP's super globals.
     *
     * @return static
     */
    public static function createFromGlobals()
    {
        $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

        if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
            && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
        ) {
            parse_str($request->getContent(), $data);
            $request->request = new ParameterBag($data);
        }

        return $request;
    }

記述をみると分かるように、createRequestFromFactory()の引数として、スーパーグローバル変数(関心のある$_POST含め)が渡されている。

コメントを見ても、Creates a new request with values from PHP's super globals.とあり、この関数において、スーパーグローバル変数に格納されたリクエスト情報を受け取っていることが分かった。

HTTPリクエスト情報がどこに格納されるのか、ようやく姿を現した次第である。

引数として渡されたスーパーグローバル変数が、どのようにしてRequestオブジェクトに変換されるのかをさらに追いたいため、引き続きcreateRequestFromFactory()ソースコードを調査する。

createRequestFromFactory()を追う

こちらの関数も、同じくSymfony\Component\HttpFoundation\Requestに静的メソッドとして定義されている。

記述としては、下記である。

    private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
    {
        if (self::$requestFactory) {
            $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content);

            if (!$request instanceof self) {
                throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
            }

            return $request;
        }

        return new static($query, $request, $attributes, $cookies, $files, $server, $content);
    }

第2引数のarray $request = []が、スーパーグローバル変数の$_POSTに該当する。

冒頭のself::$requestFactoryだが、これは同ファイル内の下記に詳しく記述されている。

    /**
     * Sets a callable able to create a Request instance.
     *
     * This is mainly useful when you need to override the Request class
     * to keep BC with an existing system. It should not be used for any
     * other purpose.
     *
     * @param callable|null $callable A PHP callable
     */
    public static function setFactory($callable)
    {
        self::$requestFactory = $callable;
    }

This is mainly useful when you need to override the Request class * to keep BC with an existing system.

記述、コメントを見ると、Requestオブジェクト生成ロジックを拡張したい時に、上記メソッドによってクラスを差し替えるようである。今回は拡張予定はないので一旦スルー。

さて、createRequestFromFactory()の最後に、重要な記述がある。

return new static($query, $request, $attributes, $cookies, $files, $server, $content);

staticなので、自分自身(ここではSymfony\Component\HttpFoundation\Requestクラス)のインスタンスを新たに生成している。そして、引数として渡されている$requestは、スーパーグローバル変数の$_POSTである。ここでもう一度、Symfony\Component\HttpFoundation\Requestクラスの__construct()に戻ってみよう。

    public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
    {
        $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
    }

    public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
    {
        $this->request = new ParameterBag($request);
        $this->query = new ParameterBag($query);
        $this->attributes = new ParameterBag($attributes);
        $this->cookies = new ParameterBag($cookies);
        $this->files = new FileBag($files);
        $this->server = new ServerBag($server);
        $this->headers = new HeaderBag($this->server->getHeaders());

        $this->content = $content;
        $this->languages = null;
        $this->charsets = null;
        $this->encodings = null;
        $this->acceptableContentTypes = null;
        $this->pathInfo = null;
        $this->requestUri = null;
        $this->baseUrl = null;
        $this->basePath = null;
        $this->method = null;
        $this->format = null;
    }

なるほど。一度見た記述だが、上記の引数として渡される$request・ひいてはSymfony\Component\HttpFoundation\Requestクラスの$requestプロパティの実体は、スーパーグローバル変数の$_POSTであることが分かった。

createFromGlobals()の調査に戻る

少し話が逸れてしまったので、再びcreateFromGlobals()の調査に戻る。

分かったこととしては、下記の$requestには、結局Symfony\Component\HttpFoundation\Requestクラスのインスタンスが返却され、プロパティの$requestにはにはスーパーグローバル変数の$_POSTを保有している、ということだ。

    public static function createFromGlobals()
    {
        $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

        if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
            && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
        ) {
            parse_str($request->getContent(), $data);
            $request->request = new ParameterBag($data);
        }

        return $request;
    }

$request以下の記述では、リクエストヘッダ情報を取得し(すでにスーパーグローバル変数として渡されている)、CONTENT_TYPE: application/x-www-form-urlencodedだった場合にエンコーディング処理を施すなどしている。

parse_str($request->getContent(), $data);

https://www.php.net/manual/ja/function.parse-str.php

ちなみに、リクエストメソッド情報も判定されているが、POSTメソッドは含まれていないようなので一旦スルー。

いずれにせよ、スーパーグローバル変数の情報が含まれた、Symfony\Component\HttpFoundation\Requestクラスのインスタンスが返却されることが分かったところで、Illuminate\Http\Requestのcapture()の記述に戻る。

Illuminate\Http\Request::capture()を再度調査

    public static function capture()
    {
        static::enableHttpMethodParameterOverride();

        return static::createFromBase(SymfonyRequest::createFromGlobals());
    }

さて、次はcreateFromBase()の調査である。同ファイルに定義されているので、こちらのソースコードを追ってみる。

    /**
     * Create an Illuminate request from a Symfony instance.
     *
     * @param  \Symfony\Component\HttpFoundation\Request  $request
     * @return static
     */
    public static function createFromBase(SymfonyRequest $request)
    {
        if ($request instanceof static) {
            return $request;
        }

        $newRequest = (new static)->duplicate(
            $request->query->all(), $request->request->all(), $request->attributes->all(),
            $request->cookies->all(), $request->files->all(), $request->server->all()
        );

        $newRequest->headers->replace($request->headers->all());

        $newRequest->content = $request->content;

        $newRequest->request = $newRequest->getInputSource();

        return $newRequest;
    }

着目したいのは、$newRequestが生成される箇所である。引数として渡されている、Symfony\Component\HttpFoundation\Requestインスタンス$requestが内部で使われている。

そこで、duplicate()を追ってみたところ、どうやらこれも、Symfony\Component\HttpFoundation\Requestに処理の本体が定義されているようである。

    /**
     * Clones a request and overrides some of its parameters.
     *
     * @param array $query      The GET parameters
     * @param array $request    The POST parameters
     * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
     * @param array $cookies    The COOKIE parameters
     * @param array $files      The FILES parameters
     * @param array $server     The SERVER parameters
     *
     * @return static
     */
    public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
    {
        $dup = clone $this;
        if (null !== $query) {
            $dup->query = new ParameterBag($query);
        }
        if (null !== $request) {
            $dup->request = new ParameterBag($request);
        }
        if (null !== $attributes) {
            $dup->attributes = new ParameterBag($attributes);
        }
        if (null !== $cookies) {
            $dup->cookies = new ParameterBag($cookies);
        }
        if (null !== $files) {
            $dup->files = new FileBag($files);
        }
        if (null !== $server) {
            $dup->server = new ServerBag($server);
            $dup->headers = new HeaderBag($dup->server->getHeaders());
        }
        $dup->languages = null;
        $dup->charsets = null;
        $dup->encodings = null;
        $dup->acceptableContentTypes = null;
        $dup->pathInfo = null;
        $dup->requestUri = null;
        $dup->baseUrl = null;
        $dup->basePath = null;
        $dup->method = null;
        $dup->format = null;

        if (!$dup->get('_format') && $this->get('_format')) {
            $dup->attributes->set('_format', $this->get('_format'));
        }

        if (!$dup->getRequestFormat(null)) {
            $dup->setRequestFormat($this->getRequestFormat(null));
        }

        return $dup;
    }

処理としては、各種情報をnew ParameterBag($request);などに置き換えているようだ。

ここまで追ってきたが、どうやらnew ParameterBag($request);が、Requestオブジェクトを生成している本体らしいことが見えてきた。

最後に、上記を調査する。

ParameterBagを追ってみる

調査中