diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdc7504..3d8abe3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,12 @@ jobs: strategy: fail-fast: true matrix: - php: [8.1, 8.2] + php: [8.1, 8.2, 8.3] laravel: [10] steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -34,7 +34,7 @@ jobs: run: composer require "laravel/framework:^${{ matrix.laravel }}" --no-update - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 38dfa04..c4ba7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [3.2.0] - 2024-02-14 + +### 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/Console/Concerns/ResolvesStub.php b/src/Console/Concerns/ResolvesStub.php index fc63283..547c93f 100644 --- a/src/Console/Concerns/ResolvesStub.php +++ b/src/Console/Concerns/ResolvesStub.php @@ -1,6 +1,6 @@ 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..ac379f4 100644 --- a/src/Routing/PendingResourceRegistration.php +++ b/src/Routing/PendingResourceRegistration.php @@ -1,6 +1,6 @@ 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/PendingServerRegistration.php b/src/Routing/PendingServerRegistration.php index 9863594..ed29aaf 100644 --- a/src/Routing/PendingServerRegistration.php +++ b/src/Routing/PendingServerRegistration.php @@ -1,6 +1,6 @@ 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/Relationships.php b/src/Routing/Relationships.php index 7d9dfd1..1890f86 100644 --- a/src/Routing/Relationships.php +++ b/src/Routing/Relationships.php @@ -1,6 +1,6 @@ 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/src/Routing/Route.php b/src/Routing/Route.php index 1d27826..01b1d24 100644 --- a/src/Routing/Route.php +++ b/src/Routing/Route.php @@ -1,6 +1,6 @@ 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..8ea35e6 100644 --- a/tests/lib/Integration/Routing/HasOneTest.php +++ b/tests/lib/Integration/Routing/HasOneTest.php @@ -1,6 +1,6 @@ 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..5287a65 100644 --- a/tests/lib/Integration/Routing/ResourceTest.php +++ b/tests/lib/Integration/Routing/ResourceTest.php @@ -1,6 +1,6 @@ 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 diff --git a/tests/lib/Integration/Routing/TestCase.php b/tests/lib/Integration/Routing/TestCase.php index 6aa5e45..9c20670 100644 --- a/tests/lib/Integration/Routing/TestCase.php +++ b/tests/lib/Integration/Routing/TestCase.php @@ -1,6 +1,6 @@