LoginSignup
17
15

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-12-20

【2021/10/15 追記】現在の自分の思想としてはこのアーキテクチャを推奨しておりません!

はじめに

【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;
 }

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

17
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
15