Edited at

【Laravel】 ルーティングサービスを継承する


はじめに

【Laravel】Route Model Binding と Policy だけで認可処理を完結させるための Resourceful ルーティングの工夫 - Qiita

せっかくここで面白いアイデアを思いついたのに,Laravelの微妙な仕様のせいで実現できずにこのまま諦める…というのは悔しかったので,ルーティングサービス一式全部継承してやろうじゃないか!というところに落ち着きました。

今回継承が必要なのは以下の4クラスです。


ベースを作る


サービス定義


app/Services/Routing/Route.php

<?php

declare(strict_types=1);

namespace App\Services\Routing;

use Illuminate\Routing\Route as BaseRoute;

class Route extends BaseRoute
{
}



app/Services/Routing/ControllerDispatcher.php

<?php

declare(strict_types=1);

namespace App\Services\Routing;

use Illuminate\Routing\ControllerDispatcher as BaseControllerDispatcher;

class ControllerDispatcher extends BaseControllerDispatcher
{
}



app/Services/Routing/Router.php

<?php

declare(strict_types=1);

namespace App\Services\Routing;

use Illuminate\Routing\Router as BaseRouter;

class Router extends BaseRouter
{
/**
* Create a new Route object.
*
* @param array|string $methods
* @param string $uri
* @param mixed $action
* @return Route
*/

protected function newRoute($methods, $uri, $action): Route
{
return (new Route($methods, $uri, $action))
->setRouter($this)
->setContainer($this->container);
}
}




  • Router および ControllerDispatcher はコンテナ経由でインスタンスが生成されるので,サービスプロバイダで登録するだけで済みます。


  • Route はクラス名が Router の中でハードコーディングされているので, Router::newRoute() のオーバーライドが必要です。


サービスプロバイダ定義


app/Providers/RoutingServiceProvider.php

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Services\Routing\ControllerDispatcher;
use App\Services\Routing\Router;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;
use Illuminate\Routing\RoutingServiceProvider as ServiceProvider;

class RoutingServiceProvider extends ServiceProvider
{
/**
* Register the router instance.
*/

protected function registerRouter(): void
{
$this->app->singleton('router', function ($app) {
return new Router($app['events'], $app);
});
}

/**
* Register the controller dispatcher.
*/

protected function registerControllerDispatcher(): void
{
$this->app->singleton(ControllerDispatcherContract::class, function ($app) {
return new ControllerDispatcher($app);
});
}
}


<?php

return [

// ...

/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/

'providers' => [

// ...

/*
* Application Service Providers...
*/

App\Providers\RoutingServiceProvider::class,

// ...
],

];


HTTPカーネルでのミドルウェアリロード


app\Http\Kernel.php

<?php

declare(strict_types=1);

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
// ...

/**
* Router をオーバーライドしているため,dispatch する前にミドルウェアを再設定します。
*
* @return \Closure
*/

protected function dispatchToRouter(): \Closure
{
$this->router = $this->app['router'];

$this->router->middlewarePriority = $this->middlewarePriority;

foreach ($this->middlewareGroups as $key => $middleware) {
$this->router->middlewareGroup($key, $middleware);
}

foreach ($this->routeMiddleware as $key => $middleware) {
$this->router->aliasMiddleware($key, $middleware);
}

return parent::dispatchToRouter();
}
}


↑これが超大事! Router の初期化は早い段階で行われているため,差し替えたあとには再度ミドルウェアをリロードする必要があります。


拡張する


RouteDependencyResolverTrait にパッチを当てるトレイト


app/Services/Routing/Concerns/AcceptsNullableDependencies.php

<?php

declare(strict_types=1);

namespace App\Services\Routing\Concerns;

use Illuminate\Contracts\Container\Container;
use Illuminate\Routing\RouteDependencyResolverTrait;
use ReflectionFunctionAbstract;
use ReflectionParameter;

/**
* Trait AcceptsNullableDependencies
*
* https://github.com/laravel/framework/pull/22488
*
* @property Container $container
* @mixin RouteDependencyResolverTrait
*/

trait AcceptsNullableDependencies
{
/**
* Resolve the given method's type-hinted dependencies.
*
* @param array $parameters
* @param ReflectionFunctionAbstract $reflector
* @return array
*/

public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
$instanceCount = 0;

$values = array_values($parameters);

foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency(
$parameter, $parameters
);

if ($instance !== '__UNDEFINED__') {
++$instanceCount;

$this->spliceIntoParameters($parameters, $key, $instance);
} elseif (!isset($values[$key - $instanceCount]) &&
$parameter->isDefaultValueAvailable()) {
$this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
}
}

return $parameters;
}

/**
* Attempt to transform the given parameter into a class instance.
*
* @param ReflectionParameter $parameter
* @param array $parameters
* @return mixed
*/

protected function transformDependency(ReflectionParameter $parameter, $parameters)
{
$class = $parameter->getClass();

// If the parameter has a type-hinted class, we will check to see if it is already in
// the list of parameters. If it is we will just skip it as it is probably a model
// binding and we do not want to mess with those; otherwise, we resolve it here.
if ($class && !$this->alreadyInParameters($class->name, $parameters)) {
return $parameter->isDefaultValueAvailable()
? $parameter->getDefaultValue()
: $this->container->make($class->name);
}

return '__UNDEFINED__';
}
}


もとの実装と違って,未解決を '__UNDEFINED__' という文字列で表現することで, null と区別できるようにしています。後はこれを RouteControllerDispatcher に当てるだけ。


app/Services/Routing/Route.php

 <?php

declare(strict_types=1);

namespace App\Services\Routing;

+use App\Services\Routing\Concerns\AcceptsNullableDependencies;
use Illuminate\Routing\Route as BaseRoute;

class Route extends BaseRoute
{
+ use AcceptsNullableDependencies;
}



app/Services/Routing/ControllerDispatcher.php

 <?php

declare(strict_types=1);

namespace App\Services\Routing;

+use App\Services\Routing\Concerns\AcceptsNullableDependencies;
use Illuminate\Routing\ControllerDispatcher as BaseControllerDispatcher;

class ControllerDispatcher extends BaseControllerDispatcher
{
+ use AcceptsNullableDependencies;
}


これで記事で書かれた内容も動くようになります。