Closed2

Laravelのcontrollerのactionが呼ばれるまでを覗きたい

shira79shira79

トレースを見ながらやると良いかも

controllerのメソッドが呼ばれるまで

public/index.phpからみてく。

kernelをコンテナに登録。
bootstrap/app.php

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

public/index.php

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

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

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

$kernel->handleを実行してる

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php

/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}

ここのthenをみる。

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

thenが何やってるのか、わからん。けど引数はdestinationだっって。

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

dispatchToRouterこっちをおうぞ

/**
 * Get the route dispatcher callback.
 *
 * @return \Closure
 */
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);

        return $this->router->dispatch($request);
    };
}

vendor/laravel/framework/src/Illuminate/Routing/Router.php

/**
 * Dispatch the request to the application.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
 */
public function dispatch(Request $request)
{
    $this->currentRequest = $request;

    return $this->dispatchToRoute($request);
}
/**
 * Return the response for the given route.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Routing\Route  $route
 * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
 */
protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

/**
 * Return the response for the given route.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Routing\Route  $route
 * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
 */
protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
}

vendor/laravel/framework/src/Illuminate/Routing/Route.php

/**
 * Run the route action and return the response.
 *
 * @return mixed
 */
public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

controllerDispatcher()でcontrollerDispatcherクラスが呼ばれて、その中のdispatchを実行してる。
controllerとmethodの名を引数。

protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}

引数のcontrollerはコンテナ経由で実行してる。

/**
 * Get the controller instance for the route.
 *
 * @return mixed
 */
public function getController()
{
    if (! $this->controller) {
        $class = $this->parseControllerCallback()[0];

        $this->controller = $this->container->make(ltrim($class, '\\'));
    }

    return $this->controller;
}
/**
 * Get the dispatcher for the route's controller.
 *
 * @return \Illuminate\Routing\Contracts\ControllerDispatcher
 */
public function controllerDispatcher()
{
    if ($this->container->bound(ControllerDispatcherContract::class)) {
        return $this->container->make(ControllerDispatcherContract::class);
    }

    return new ControllerDispatcher($this->container);
}

vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php

/**
 * Dispatch a request to a given controller and method.
 *
 * @param  \Illuminate\Routing\Route  $route
 * @param  mixed  $controller
 * @param  string  $method
 * @return mixed
 */
public function dispatch(Route $route, $controller, $method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    if (method_exists($controller, 'callAction')) {
        return $controller->callAction($method, $parameters);
    }

    return $controller->{$method}(...array_values($parameters));
}

ここで実行された!!!!!
controllerには、callActionが実装されてる。

vendor/laravel/framework/src/Illuminate/Routing/Controller.php

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

ここの$parametersって何がはいってるんだろうっていうのを、次の段落でみていく

parameterのタイプヒントはどうやってるのかな?を探る

parameterを探してく、。

さっきも出した、

public function dispatch(Route $route, $controller, $method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    if (method_exists($controller, 'callAction')) {
        return $controller->callAction($method, $parameters);
    }

    return $controller->{$method}(...array_values($parameters));
}

vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php

{
/**
 * Resolve the object method's type-hinted dependencies.
 *
 * @param  array  $parameters
 * @param  object  $instance
 * @param  string  $method
 * @return array
 */
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
    if (! method_exists($instance, $method)) {
        return $parameters;
    }

    return $this->resolveMethodDependencies(
        $parameters, new ReflectionMethod($instance, $method)
    );
}

controllerにmethodが存在しなかったら、そのまま返す。
もし存在してたら、resolveMethodDependenciesが依存を解決してparametersをまた返す。

この関数が何やってるかは追うのキツそうなのでシカトする。
ちょっとみた感じ、parameterのデフォルト値が存在しなかったら、コンテナでmakeされて、それをparameterに戻してあげる??みたいなことをやっそう。array_sliceとかしてるねえ。

じゃあその元の値は$route->parametersWithoutNulls()

null値を排除。

public function parametersWithoutNulls()
{
    return array_filter($this->parameters(), function ($p) {
        return ! is_null($p);
    });
}

ここで取得してる。

public function parameters()
{
    if (isset($this->parameters)) {
        return $this->parameters;
    }

    throw new LogicException('Route is not bound.');
}

$this->parametersは、どこで登録してるねん。

次はどこで登録してるのかみてく

Route.php

vendor/laravel/framework/src/Illuminate/Routing/Route.phpparametersっていうプロパティのセットをおう。

最終的には、以下の関数が呼ばれてるみたい。なのでこれがどこで呼ばれてるかってのを見る。

/**
 * Set a parameter to the given value.
 *
 * @param  string  $name
 * @param  mixed   $value
 * @return void
 */
public function setParameter($name, $value)
{
    $this->parameters();

    $this->parameters[$name] = $value;
}

んで、見つけたので順を追ってみていく。

これの登録はmiddlewareでやってるくさい

app/Http/Kernel.php

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php

<?php

namespace Illuminate\Routing\Middleware;

use Closure;
use Illuminate\Contracts\Routing\Registrar;

class SubstituteBindings
{
    /**
     * The router instance.
     *
     * @var \Illuminate\Contracts\Routing\Registrar
     */
    protected $router;

    /**
     * Create a new bindings substitutor.
     *
     * @param  \Illuminate\Contracts\Routing\Registrar  $router
     * @return void
     */
    public function __construct(Registrar $router)
    {
        $this->router = $router;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $this->router->substituteBindings($route = $request->route());

        $this->router->substituteImplicitBindings($route);

        return $next($request);
    }
}

substituteBindingsの実態

vendor/laravel/framework/src/Illuminate/Routing/Router.php

こっちは素通り?

/**
 * Substitute the route bindings onto the route.
 *
 * @param  \Illuminate\Routing\Route  $route
 * @return \Illuminate\Routing\Route
 *
 * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
 */
public function substituteBindings($route)
{
    foreach ($route->parameters() as $key => $value) {
        if (isset($this->binders[$key])) {
            $route->setParameter($key, $this->performBinding($key, $value, $route));
        }
    }

    return $route;
}

こっち!!!!!!!!!!

/**
 * Substitute the implicit Eloquent model bindings for the route.
 *
 * @param  \Illuminate\Routing\Route  $route
 * @return void
 *
 * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
 */
public function substituteImplicitBindings($route)
{
    ImplicitRouteBinding::resolveForRoute($this->container, $route);
}

こっからは行けそう。

$route->signatureParametersで引数を配列で取得。それをforeachで回して、タイプヒントからコンテナ経由で実態を取得。
それを$route->setParameterでセットしてる。
vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php

/**
 * Resolve the implicit route bindings for the given route.
 *
 * @param  \Illuminate\Container\Container  $container
 * @param  \Illuminate\Routing\Route  $route
 * @return void
 *
 * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
 */
public static function resolveForRoute($container, $route)
{
    $parameters = $route->parameters();

    foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
        if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
            continue;
        }

        $parameterValue = $parameters[$parameterName];

        if ($parameterValue instanceof UrlRoutable) {
            continue;
        }

        $instance = $container->make($parameter->getClass()->name);

        if (! $model = $instance->resolveRouteBinding($parameterValue)) {
            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
        }

        $route->setParameter($parameterName, $model);
    }
}

一番したにsetParameterいましたね。
foreachしてる配列はここで取得。
vendor/laravel/framework/src/Illuminate/Routing/Route.php

/**
 * Get the parameters that are listed in the route / controller signature.
 *
 * @param  string|null  $subClass
 * @return array
 */
public function signatureParameters($subClass = null)
{
    return RouteSignatureParameters::fromAction($this->action, $subClass);
}

vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php

/**
 * Extract the route action's signature parameters.
 *
 * @param  array  $action
 * @param  string|null  $subClass
 * @return array
 */
public static function fromAction(array $action, $subClass = null)
{
    $parameters = is_string($action['uses'])
                    ? static::fromClassMethodString($action['uses'])
                    : (new ReflectionFunction($action['uses']))->getParameters();

    return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) {
        return $p->getClass() && $p->getClass()->isSubclassOf($subClass);
    });
}
/**
 * Get the parameters for the given class / method by string.
 *
 * @param  string  $uses
 * @return array
 */
protected static function fromClassMethodString($uses)
{
    [$class, $method] = Str::parseCallback($uses);

    if (! method_exists($class, $method) && is_callable($class, $method)) {
        return [];
    }

    return (new ReflectionMethod($class, $method))->getParameters();
}
public static function parseCallback($callback, $default = null)
{
    return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default];
}

routeでusesを指定してたら簡単ですね。

Route::get('/', [
    'as'   => 'top.index',
    'uses' => 'TopController@index',
]);

ただ、下みたいな、上に当てはまらない場合はどうしてるんだろうね。

Route::get('/', 'TopController@index')->name('top.index');

ちゃんちゃん。

おまけ

俺はてっきり、こいつから呼ばれてるのかと思ってた
vendor/laravel/framework/src/Illuminate/Container/Container.php

/**
 * Call the given Closure / class@method and inject its dependencies.
 *
 * @param  callable|string  $callback
 * @param  array  $parameters
 * @param  string|null  $defaultMethod
 * @return mixed
 */
public function call($callback, array $parameters = [], $defaultMethod = null)
{
    return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}

匂わせなメソッドも通っていたので。
vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php

/**
 * Determine if the given string is in Class@method syntax.
 *
 * @param  mixed  $callback
 * @return bool
 */
protected static function isCallableWithAtSign($callback)
{
    return is_string($callback) && strpos($callback, '@') !== false;
}

「'@'」で検索するとcontrollerのactionで通ってそうな関数が見つかる、、。なんやねんこいつら。

このスクラップは2022/02/15にクローズされました