Laravel のルート定義をメソッドチェインでいい感じに書けるようにするやつ

前に [Laravel のルート定義でコントローラーのクラス名とメソッド名を別々に書きたい](https://qiita.com/ngyuki/items/17de494e618b2eaa403e) みたいなのを書いた後、それっぽくルート定義を書けるようにするためのクラスを作りました。

マジックメソッドでミドルウェアがシュッと書けるので、ミドルウェアの記述がスッキリします。

```php
<?php
namespace App\Routing;

use Illuminate\Routing\Router;

/**
 * @method $this auth()
 * @method $this can(string $arg)
 */
class AppRouteCollector
{
    /**
     * @var Router
     */
    private $router;

    /**
     * @var array
     */
    private $methods = [];

    /**
     * @var array
     */
    private $paths = [];

    /**
     * @var array
     */
    private $actions = [];

    /**
     * @var array
     */
    private $names = [];

    /**
     * @var array
     */
    private $middlewares = [];

    /**
     * @var array
     */
    private $wheres = [];

    /**
     * @var bool
     */
    private $last = true;

    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    public function __destruct()
    {
        if (!$this->last) {
            return;
        }

        $this->router->match(
            self::generateMethods($this->methods),
            self::generatePath($this->paths),
            [
                'uses' => self::generateUses($this->actions),
                'as' => self::generateAs($this->names),
                'middleware' => self::generateMiddleware($this->middlewares),
                'where' => self::generateWhere($this->wheres),
            ]
        );
    }

    private static function generateMethods(array $methods): array
    {
        $methods = array_map(function (string $method) {
            return strtoupper($method);
        }, $methods);
        return $methods;
    }

    private static function generatePath(array $paths): string
    {
        $paths = array_map(function (string $path) {
            return trim($path, '/');
        }, $paths);
        $paths = array_filter($paths, function (string $path) {
            return strlen($path) > 0;
        });
        return '/' . implode('/', $paths);
    }

    private static function generateUses(array $actions): string
    {
        $actions = array_map(function (string $action) {
            return trim($action, '\\');
        }, $actions);

        if ($actions && end($actions)[0] === '@') {
            $method = array_pop($actions);
            $actions = implode('\\', $actions) . $method;
        } else {
            $actions = implode('\\', $actions);
        }

        return $actions;
    }

    private static function generateAs(array $names): string
    {
        return implode('', $names);
    }

    private static function generateMiddleware(array $middlewares): array
    {
        $arr = [];
        foreach ($middlewares as $middleware => $arg) {
            if (is_bool($arg)) {
                $arg = var_export($arg, true);
            }
            if ($arg !== null) {
                $middleware = "$middleware:$arg";
            }
            $arr[] = $middleware;
        }
        return $arr;
    }

    private static function generateWhere(array $wheres): array
    {
        return $wheres;
    }

    /**
     * @return static
     */
    private function chain()
    {
        $this->last = false;
        $new = clone $this;
        $new->last = true;
        return $new;
    }

    /**
     * @param string $method
     * @param string $path
     *
     * @return static
     */
    public function method(string $method, string $path)
    {
        $new = $this->path($path);
        $new->methods[] = strtoupper($method);
        return $new;
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function get(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function post(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function put(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function patch(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function delete(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function options(string $path)
    {
        return $this->method(__FUNCTION__, $path);
    }

    /**
     * @param string $path
     *
     * @return static
     */
    public function path($path)
    {
        $new = $this->chain();
        $new->paths[] = trim($path, '/');
        return $new;
    }

    /**
     * @param string $action
     *
     * @return static
     */
    public function action(string $action)
    {
        $new = $this->chain();
        $new->actions[] = $action;
        return $new;
    }

    /**
     * @param string $name
     *
     * @return static
     */
    public function name(string $name)
    {
        $new = $this->chain();
        $new->names[] = $name;
        return $new;
    }

    /**
     * @param string $middleware
     * @param mixed  $arg
     *
     * @return static
     */
    public function middleware(string $middleware, $arg = null)
    {
        $new = $this->chain();
        $new->middlewares[$middleware] = $arg;
        return $new;
    }

    /**
     * @param string $where
     * @param string $expr
     *
     * @return static
     */
    public function where(string $where, string $expr)
    {
        $new = $this->chain();
        $new->wheres[$where] = $expr;
        return $new;
    }

    /**
     * @param string $name
     * @param array  $arguments
     *
     * @return static
     */
    public function __call($name, $arguments)
    {
        return $this->middleware($name, $arguments[0] ?? null);
    }

    /**
     * @param string $name
     *
     * @return static
     */
    public function __get($name)
    {
        return $this->middleware($name);
    }

    /**
     * @param callable $callback
     */
    public function group(callable $callback): void
    {
        $new = $this->chain();
        $new->last = false;
        $callback($new);
    }
}
```

次のように使います。`middleware('can:admin')` が `can('admin')` のように書けます。静的解析にも優しいです。

```php
use App\Routing\AppRouteCollector as R;

$r = resolve(R::class);

$r->auth()->group(function (R $r) {
    $r->action(UserController::class)->can('admin')->group(function (R $r) {
        $r->get('/users')->action('@index')->name('user.list');
        $r->get('/users/create')->action('@create')->name('user.create');
        $r->post('/users')->action('@store')->name('user.store');
        $r->get('/users/{user}')->action('@show')->name('user.show');
        $r->get('/users/{user}/edit')->action('@edit')->name('user.edit');
        $r->put('/users/{user}')->action('@update')->name('user.update');
        $r->delete('/users/{user}')->action('@destroy')->name('user.destroy');
    });
});
```

## さいごに

ついカッとなって作ったものの、我に返って冷静になると別に素のままの Laravel のルート定義でも良いか・・・と思ったのでお蔵入り。