diff --git a/CHANGELOG.md b/CHANGELOG.md index 38dfa04..6f7e4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +### Added + +- [#265](https://github.com/laravel-json-api/laravel/issues/265) Allow registration of middleware per action on both + resource routes and relationship routes. + ## [3.2.0] - 2023-11-08 ### Added diff --git a/src/Routing/PendingRelationshipRegistration.php b/src/Routing/PendingRelationshipRegistration.php index d0a5934..b10c97d 100644 --- a/src/Routing/PendingRelationshipRegistration.php +++ b/src/Routing/PendingRelationshipRegistration.php @@ -20,6 +20,7 @@ namespace LaravelJsonApi\Laravel\Routing; use Illuminate\Routing\RouteCollection; +use Illuminate\Support\Arr; class PendingRelationshipRegistration { @@ -155,12 +156,30 @@ public function name(string $method, string $name): self /** * Add middleware to the resource routes. * - * @param string ...$middleware + * @param mixed ...$middleware * @return $this */ - public function middleware(string ...$middleware): self + public function middleware(...$middleware): self { - $this->options['middleware'] = $middleware; + if (count($middleware) === 1) { + $middleware = Arr::wrap($middleware[0]); + } + + if (array_is_list($middleware)) { + $this->options['middleware'] = $middleware; + return $this; + } + + $this->options['middleware'] = Arr::wrap($middleware['*'] ?? null); + + foreach ($this->map as $alias => $action) { + if (isset($middleware[$alias])) { + $middleware[$action] = $middleware[$alias]; + unset($middleware[$alias]); + } + } + + $this->options['action_middleware'] = $middleware; return $this; } diff --git a/src/Routing/PendingResourceRegistration.php b/src/Routing/PendingResourceRegistration.php index d07c8f3..9969690 100644 --- a/src/Routing/PendingResourceRegistration.php +++ b/src/Routing/PendingResourceRegistration.php @@ -21,6 +21,7 @@ use Closure; use Illuminate\Routing\RouteCollection; +use Illuminate\Support\Arr; use InvalidArgumentException; use function is_string; @@ -180,12 +181,22 @@ public function parameter(string $parameter): self /** * Add middleware to the resource routes. * - * @param string ...$middleware + * @param mixed ...$middleware * @return $this */ - public function middleware(string ...$middleware): self + public function middleware(...$middleware): self { - $this->options['middleware'] = $middleware; + if (count($middleware) === 1) { + $middleware = Arr::wrap($middleware[0]); + } + + if (array_is_list($middleware)) { + $this->options['middleware'] = $middleware; + return $this; + } + + $this->options['middleware'] = Arr::wrap($middleware['*'] ?? null); + $this->options['action_middleware'] = $middleware; return $this; } @@ -196,7 +207,7 @@ public function middleware(string ...$middleware): self * @param string ...$middleware * @return $this */ - public function withoutMiddleware(string ...$middleware) + public function withoutMiddleware(string ...$middleware): self { $this->options['excluded_middleware'] = array_merge( (array) ($this->options['excluded_middleware'] ?? []), diff --git a/src/Routing/RelationshipRegistrar.php b/src/Routing/RelationshipRegistrar.php index a9aae8a..2aa2363 100644 --- a/src/Routing/RelationshipRegistrar.php +++ b/src/Routing/RelationshipRegistrar.php @@ -22,6 +22,7 @@ use Illuminate\Contracts\Routing\Registrar as RegistrarContract; use Illuminate\Routing\Route as IlluminateRoute; use Illuminate\Routing\RouteCollection; +use Illuminate\Support\Arr; use LaravelJsonApi\Contracts\Schema\Schema; use LaravelJsonApi\Core\Support\Str; @@ -272,9 +273,10 @@ private function getRelationshipAction( $name = $this->getRelationRouteName($method, $defaultName, $options); $action = ['as' => $name, 'uses' => $this->controller.'@'.$method]; + $middleware = $this->getMiddleware($method, $options); - if (isset($options['middleware'])) { - $action['middleware'] = $options['middleware']; + if (!empty($middleware)) { + $action['middleware'] = $middleware; } if (isset($options['excluded_middleware'])) { @@ -284,6 +286,22 @@ private function getRelationshipAction( return $action; } + /** + * @param string $action + * @param array $options + * @return array + */ + private function getMiddleware(string $action, array $options): array + { + $all = $options['middleware'] ?? []; + $actions = $options['action_middleware'] ?? []; + + return [ + ...$all, + ...Arr::wrap($actions[$action] ?? null), + ]; + } + /** * @param string $fieldName * @return string diff --git a/src/Routing/ResourceRegistrar.php b/src/Routing/ResourceRegistrar.php index bde73d8..495a69a 100644 --- a/src/Routing/ResourceRegistrar.php +++ b/src/Routing/ResourceRegistrar.php @@ -23,6 +23,7 @@ use Illuminate\Contracts\Routing\Registrar as RegistrarContract; use Illuminate\Routing\Route as IlluminateRoute; use Illuminate\Routing\RouteCollection; +use Illuminate\Support\Arr; use LaravelJsonApi\Contracts\Server\Server; use LaravelJsonApi\Core\Support\Str; @@ -337,13 +338,14 @@ private function getResourceAction( string $method, ?string $parameter, array $options - ) { + ): array { $name = $this->getResourceRouteName($resourceType, $method, $options); $action = ['as' => $name, 'uses' => $controller.'@'.$method]; + $middleware = $this->getMiddleware($method, $options); - if (isset($options['middleware'])) { - $action['middleware'] = $options['middleware']; + if (!empty($middleware)) { + $action['middleware'] = $middleware; } if (isset($options['excluded_middleware'])) { @@ -355,6 +357,22 @@ private function getResourceAction( return $action; } + /** + * @param string $action + * @param array $options + * @return array + */ + private function getMiddleware(string $action, array $options): array + { + $all = $options['middleware'] ?? []; + $actions = $options['action_middleware'] ?? []; + + return [ + ...$all, + ...Arr::wrap($actions[$action] ?? null), + ]; + } + /** * Get the action array for the relationships group. * diff --git a/tests/lib/Integration/Routing/HasManyTest.php b/tests/lib/Integration/Routing/HasManyTest.php index 16c3777..f2b28d5 100644 --- a/tests/lib/Integration/Routing/HasManyTest.php +++ b/tests/lib/Integration/Routing/HasManyTest.php @@ -144,13 +144,95 @@ public function testMiddleware(string $method, string $uri): void ->middleware('foo') ->resources(function ($server) { $server->resource('posts')->middleware('bar')->relationships(function ($relations) { - $relations->hasMany('tags')->middleware('baz'); + $relations->hasMany('tags')->middleware('baz1', 'baz2'); }); }); }); $route = $this->assertMatch($method, $uri); - $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz'], $route->action['middleware']); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz1', 'baz2'], $route->action['middleware']); + } + + /** + * @param string $method + * @param string $uri + * @dataProvider genericProvider + */ + public function testMiddlewareAsArrayList(string $method, string $uri): void + { + $server = $this->createServer('v1'); + $schema = $this->createSchema($server, 'posts', '\d+'); + $this->createRelation($schema, 'tags'); + + $this->defaultApiRoutesWithNamespace(function () { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function ($server) { + $server->resource('posts')->middleware('bar')->relationships(function ($relations) { + $relations->hasMany('tags')->middleware(['baz1', 'baz2']); + }); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz1', 'baz2'], $route->action['middleware']); + } + + /** + * @param string $method + * @param string $uri + * @param string $action + * @dataProvider genericProvider + */ + public function testActionMiddleware(string $method, string $uri, string $action): void + { + $actions = [ + '*' => ['baz1', 'baz2'], + 'showRelated' => 'showRelated1', + 'showRelationship' => ['showRelationship1', 'showRelationship2'], + 'updateRelationship' => 'updateRelationship1', + 'attachRelationship' => ['attachRelationship1', 'attachRelationship2'], + 'detachRelationship' => 'detachRelationship1', + ]; + + $expected = [ + 'api', + 'jsonapi:v1', + 'foo', + 'bar', + ...$actions['*'], + ...Arr::wrap($actions[$action]), + ]; + + $server = $this->createServer('v1'); + $schema = $this->createSchema($server, 'posts', '\d+'); + $this->createRelation($schema, 'tags'); + + $this->defaultApiRoutesWithNamespace(function () use ($actions) { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function ($server) use ($actions) { + $server->resource('posts')->middleware('bar')->relationships( + function ($relations) use ($actions) { + $relations->hasMany('tags')->middleware([ + '*' => $actions['*'], + 'related' => $actions['showRelated'], + 'show' => $actions['showRelationship'], + 'update' => $actions['updateRelationship'], + 'attach' => $actions['attachRelationship'], + 'detach' => $actions['detachRelationship'], + ]); + }, + ); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame($expected, $route->action['middleware']); } /** diff --git a/tests/lib/Integration/Routing/HasOneTest.php b/tests/lib/Integration/Routing/HasOneTest.php index 8a85cc6..3861988 100644 --- a/tests/lib/Integration/Routing/HasOneTest.php +++ b/tests/lib/Integration/Routing/HasOneTest.php @@ -22,6 +22,8 @@ use Illuminate\Contracts\Routing\Registrar; use LaravelJsonApi\Core\Support\Arr; use LaravelJsonApi\Laravel\Facades\JsonApiRoute; +use LaravelJsonApi\Laravel\Routing\Relationships; +use LaravelJsonApi\Laravel\Routing\ResourceRegistrar; class HasOneTest extends TestCase { @@ -117,11 +119,9 @@ public function testName(string $method, string $uri, string $action): void /** * @param string $method * @param string $uri - * @param string $action - * @param string $name * @dataProvider genericProvider */ - public function testMiddleware(string $method, string $uri, string $action, string $name): void + public function testMiddleware(string $method, string $uri): void { $server = $this->createServer('v1'); $schema = $this->createSchema($server, 'posts', '\d+'); @@ -134,13 +134,91 @@ public function testMiddleware(string $method, string $uri, string $action, stri ->middleware('foo') ->resources(function ($server) { $server->resource('posts')->middleware('bar')->relationships(function ($relations) { - $relations->hasOne('author')->middleware('baz'); + $relations->hasOne('author')->middleware('baz1', 'baz2'); }); }); }); $route = $this->assertMatch($method, $uri); - $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz'], $route->action['middleware']); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz1', 'baz2'], $route->action['middleware']); + } + + /** + * @param string $method + * @param string $uri + * @dataProvider genericProvider + */ + public function testMiddlewareAsArrayList(string $method, string $uri): void + { + $server = $this->createServer('v1'); + $schema = $this->createSchema($server, 'posts', '\d+'); + $this->createRelation($schema, 'author'); + + $this->defaultApiRoutesWithNamespace(function () { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function (ResourceRegistrar $server) { + $server->resource('posts')->middleware('bar')->relationships(function (Relationships $relations) { + $relations->hasOne('author')->middleware(['baz1', 'baz2']); + }); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar', 'baz1', 'baz2'], $route->action['middleware']); + } + + /** + * @param string $method + * @param string $uri + * @param string $action + * @dataProvider genericProvider + */ + public function testActionMiddleware(string $method, string $uri, string $action): void + { + $actions = [ + '*' => ['baz1', 'baz2'], + 'showRelated' => 'showRelated1', + 'showRelationship' => ['showRelationship1', 'showRelationship2'], + 'updateRelationship' => 'updateRelationship1', + ]; + + $expected = [ + 'api', + 'jsonapi:v1', + 'foo', + 'bar', + ...$actions['*'], + ...Arr::wrap($actions[$action]), + ]; + + $server = $this->createServer('v1'); + $schema = $this->createSchema($server, 'posts', '\d+'); + $this->createRelation($schema, 'author'); + + $this->defaultApiRoutesWithNamespace(function () use ($actions) { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function (ResourceRegistrar $server) use ($actions) { + $server->resource('posts')->middleware('bar')->relationships( + function (Relationships $relations) use ($actions) { + $relations->hasOne('author')->middleware([ + '*' => $actions['*'], + 'related' => $actions['showRelated'], + 'show' => $actions['showRelationship'], + 'update' => $actions['updateRelationship'], + ]); + }, + ); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame($expected, $route->action['middleware']); } /** diff --git a/tests/lib/Integration/Routing/ResourceTest.php b/tests/lib/Integration/Routing/ResourceTest.php index 2dad057..c657f42 100644 --- a/tests/lib/Integration/Routing/ResourceTest.php +++ b/tests/lib/Integration/Routing/ResourceTest.php @@ -23,6 +23,7 @@ use Illuminate\Contracts\Routing\Registrar; use LaravelJsonApi\Core\Support\Arr; use LaravelJsonApi\Laravel\Facades\JsonApiRoute; +use LaravelJsonApi\Laravel\Routing\ResourceRegistrar; class ResourceTest extends TestCase { @@ -217,14 +218,14 @@ public function testServerMiddleware(string $method, string $uri): void JsonApiRoute::server('v1') ->prefix('v1') ->namespace('Api\\V1') - ->middleware('foo') + ->middleware('foo', 'bar') ->resources(function ($server) { $server->resource('posts'); }); }); $route = $this->assertMatch($method, $uri); - $this->assertSame(['api', 'jsonapi:v1', 'foo'], $route->action['middleware']); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar'], $route->action['middleware']); } /** @@ -251,6 +252,97 @@ public function testResourceMiddleware(string $method, string $uri): void $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar'], $route->action['middleware']); } + /** + * @param string $method + * @param string $uri + * @dataProvider routeProvider + */ + public function testResourceWithMultipleMiddleware(string $method, string $uri): void + { + $server = $this->createServer('v1'); + $this->createSchema($server, 'posts', '\d+'); + + $this->defaultApiRoutesWithNamespace(function () { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function ($server) { + $server->resource('posts')->middleware('bar1', 'bar2'); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar1', 'bar2'], $route->action['middleware']); + } + + /** + * @param string $method + * @param string $uri + * @dataProvider routeProvider + */ + public function testResourceMiddlewareArrayList(string $method, string $uri): void + { + $server = $this->createServer('v1'); + $this->createSchema($server, 'posts', '\d+'); + + $this->defaultApiRoutesWithNamespace(function () { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function (ResourceRegistrar $server) { + $server->resource('posts')->middleware(['bar1', 'bar2']); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame(['api', 'jsonapi:v1', 'foo', 'bar1', 'bar2'], $route->action['middleware']); + } + + + /** + * @param string $method + * @param string $uri + * @param string $action + * @dataProvider routeProvider + */ + public function testResourceActionMiddleware(string $method, string $uri, string $action): void + { + $actions = [ + '*' => ['bar1', 'bar2'], + 'index' => 'index1', + 'store' => ['store1', 'store2'], + 'show' => ['show1'], + 'update' => 'update1', + 'destroy' => 'destroy1', + ]; + + $expected = [ + 'api', + 'jsonapi:v1', + 'foo', + ...$actions['*'], + ...Arr::wrap($actions[$action]), + ]; + + $server = $this->createServer('v1'); + $this->createSchema($server, 'posts', '\d+'); + + $this->defaultApiRoutesWithNamespace(function () use ($actions) { + JsonApiRoute::server('v1') + ->prefix('v1') + ->namespace('Api\\V1') + ->middleware('foo') + ->resources(function (ResourceRegistrar $server) use ($actions) { + $server->resource('posts')->middleware($actions); + }); + }); + + $route = $this->assertMatch($method, $uri); + $this->assertSame($expected, $route->action['middleware']); + } + /** * @param string $method * @param string $uri