【2021/10/15 追記】現在の自分の思想としてはこのアーキテクチャを推奨しておりません!
はじめに
【Laravel】Route Model Binding と Policy だけで認可処理を完結させるための Resourceful ルーティングの工夫 - Qiita
せっかくここで面白いアイデアを思いついたのに,Laravelの微妙な仕様のせいで実現できずにこのまま諦める…というのは悔しかったので,ルーティングサービス一式全部継承してやろうじゃないか!というところに落ち着きました。
今回継承が必要なのは以下の4クラスです。
-
Illuminate\Routing\Route
… ルート1つ1つを表現するクラス -
Illuminate\Routing\Router
… ルートの定義とアクションの実行を行うクラス -
Illuminate\Routing\ControllerDispatcher
… コントローラに関するアクションの実行を行うクラス -
Illuminate\Routing\RoutingServiceProvider
… 上記のクラス群を登録するサービスプロバイダ
ベースを作る
サービス定義
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
と区別できるようにしています。後はこれを Route
と ControllerDispatcher
に当てるだけ。
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;
}
これで記事で書かれた内容も動くようになります。