From e7559db6a78e226ce83ce4c39f6a7ad4391ab8f0 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Wed, 26 Jun 2024 20:34:12 +0100 Subject: [PATCH 01/18] fix: ensure self link is correct in related resource responses --- CHANGELOG.md | 6 ++ composer.json | 2 +- .../tests/Api/V1/Posts/ReadAuthorTest.php | 9 ++- .../tests/Api/V1/Posts/ReadCommentsTest.php | 9 +-- .../dummy/tests/Api/V1/Posts/ReadTagsTest.php | 9 +-- .../Relationships/ToManyLinksTest.php | 61 ++++++++++++++++--- .../Relationships/ToOneLinksTest.php | 59 +++++++++++++++--- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd13a4..9684c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +### Fixed + +- [core#17](https://github.com/laravel-json-api/core/pull/17) Fix incorrect `self` link in related resource responses, + and remove `related` link that should not exist. This has been incorrect for some time, but is definitely what + the [spec defines here.](https://jsonapi.org/format/1.0/#document-top-level) + ## [4.0.0] - 2024-03-14 ### Changed diff --git a/composer.json b/composer.json index a09de20..3eba78d 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^8.2", "ext-json": "*", - "laravel-json-api/core": "^4.0", + "laravel-json-api/core": "^4.1", "laravel-json-api/eloquent": "^4.0", "laravel-json-api/encoder-neomerx": "^4.0", "laravel-json-api/exceptions": "^3.0", diff --git a/tests/dummy/tests/Api/V1/Posts/ReadAuthorTest.php b/tests/dummy/tests/Api/V1/Posts/ReadAuthorTest.php index ca54ab2..c8906e4 100644 --- a/tests/dummy/tests/Api/V1/Posts/ReadAuthorTest.php +++ b/tests/dummy/tests/Api/V1/Posts/ReadAuthorTest.php @@ -42,12 +42,11 @@ public function test(): void $response = $this ->withoutExceptionHandling() ->jsonApi('users') - ->get($related = url('/api/v1/posts', [$this->post, 'author'])); + ->get($self = url('/api/v1/posts', [$this->post, 'author'])); - $response->assertFetchedOneExact($expected)->assertLinks([ - 'self' => url('/api/v1/posts', [$this->post, 'relationships', 'author']), - 'related' => $related, - ]); + $response + ->assertFetchedOneExact($expected) + ->assertLinks(['self' => $self]); } public function testFilterMatches(): void diff --git a/tests/dummy/tests/Api/V1/Posts/ReadCommentsTest.php b/tests/dummy/tests/Api/V1/Posts/ReadCommentsTest.php index f7357c1..c80f0a3 100644 --- a/tests/dummy/tests/Api/V1/Posts/ReadCommentsTest.php +++ b/tests/dummy/tests/Api/V1/Posts/ReadCommentsTest.php @@ -48,15 +48,10 @@ public function test(): void $response = $this ->withoutExceptionHandling() ->jsonApi('comments') - ->get($related = url('/api/v1/posts', [$this->post, 'comments'])); - - $links = [ - 'self' => url('/api/v1/posts', [$this->post, 'relationships', 'comments']), - 'related' => $related, - ]; + ->get($self = url('/api/v1/posts', [$this->post, 'comments'])); $response->assertFetchedMany($expected) - ->assertLinks($links) + ->assertLinks(['self' => $self]) ->assertExactMeta(['count' => 3]); } diff --git a/tests/dummy/tests/Api/V1/Posts/ReadTagsTest.php b/tests/dummy/tests/Api/V1/Posts/ReadTagsTest.php index fb2bed8..f83319e 100644 --- a/tests/dummy/tests/Api/V1/Posts/ReadTagsTest.php +++ b/tests/dummy/tests/Api/V1/Posts/ReadTagsTest.php @@ -43,11 +43,12 @@ public function test(): void $response = $this ->jsonApi('tags') - ->get(url('/api/v1/posts', [$this->post, 'tags'])); + ->get($self = url('/api/v1/posts', [$this->post, 'tags'])); - $response->assertFetchedMany($expected)->assertExactMeta([ - 'count' => count($expected) - ]); + $response + ->assertFetchedMany($expected) + ->assertExactMeta(['count' => count($expected)]) + ->assertLinks(['self' => $self]); } public function testSort(): void diff --git a/tests/lib/Acceptance/Relationships/ToManyLinksTest.php b/tests/lib/Acceptance/Relationships/ToManyLinksTest.php index c6391f9..1fc5169 100644 --- a/tests/lib/Acceptance/Relationships/ToManyLinksTest.php +++ b/tests/lib/Acceptance/Relationships/ToManyLinksTest.php @@ -15,6 +15,7 @@ use App\Models\Post; use App\Models\Tag; use Closure; +use LaravelJsonApi\Core\Document\Links; use LaravelJsonApi\Core\Facades\JsonApi; use LaravelJsonApi\Laravel\Facades\JsonApiRoute; use LaravelJsonApi\Laravel\Http\Controllers\JsonApiController; @@ -60,7 +61,7 @@ protected function setUp(): void /** * @return array[] */ - public static function scenarioProvider(): array + public static function relationshipProvider(): array { return [ 'hidden' => [ @@ -99,39 +100,81 @@ static function (PostSchema $schema, Post $post) { /** * @param Closure $scenario * @return void - * @dataProvider scenarioProvider + * @dataProvider relationshipProvider */ - public function testRelated(Closure $scenario): void + public function testRelationship(Closure $scenario): void { $expected = $scenario($this->schema, $this->post); $response = $this ->withoutExceptionHandling() ->jsonApi('tags') - ->get(url('/api/v1/posts', [$this->post, 'tags'])); + ->get(url('/api/v1/posts', [$this->post, 'relationships', 'tags'])); - $response->assertFetchedMany([$this->tag]); + $response->assertFetchedToMany([$this->tag]); if (is_array($expected)) { $response->assertLinks($expected); } } + + /** + * @return array[] + */ + public static function relatedProvider(): array + { + return [ + 'hidden' => [ + static function (PostSchema $schema) { + $schema->relationship('tags')->hidden(); + return null; + }, + ], + 'no links' => [ + static function (PostSchema $schema) { + $schema->relationship('tags')->serializeUsing( + static fn($relation) => $relation->withoutLinks() + ); + return null; + }, + ], + 'no self link' => [ + static function (PostSchema $schema, Post $post) { + $schema->relationship('tags')->serializeUsing( + static fn($relation) => $relation->withoutSelfLink() + ); + // related becomes self. + return ['self' => url('/api/v1/posts', [$post, 'tags'])]; + }, + ], + 'no related link' => [ + static function (PostSchema $schema, Post $post) { + $schema->relationship('tags')->serializeUsing( + static fn($relation) => $relation->withoutRelatedLink() + ); + // related becomes self, but it's missing so we can't do that. + return null; + }, + ], + ]; + } + /** * @param Closure $scenario * @return void - * @dataProvider scenarioProvider + * @dataProvider relatedProvider */ - public function testSelf(Closure $scenario): void + public function testRelated(Closure $scenario): void { $expected = $scenario($this->schema, $this->post); $response = $this ->withoutExceptionHandling() ->jsonApi('tags') - ->get(url('/api/v1/posts', [$this->post, 'relationships', 'tags'])); + ->get(url('/api/v1/posts', [$this->post, 'tags'])); - $response->assertFetchedToMany([$this->tag]); + $response->assertFetchedMany([$this->tag]); if (is_array($expected)) { $response->assertLinks($expected); diff --git a/tests/lib/Acceptance/Relationships/ToOneLinksTest.php b/tests/lib/Acceptance/Relationships/ToOneLinksTest.php index 65484d3..a931654 100644 --- a/tests/lib/Acceptance/Relationships/ToOneLinksTest.php +++ b/tests/lib/Acceptance/Relationships/ToOneLinksTest.php @@ -53,7 +53,7 @@ protected function setUp(): void /** * @return array[] */ - public static function scenarioProvider(): array + public static function relationshipProvider(): array { return [ 'hidden' => [ @@ -92,39 +92,80 @@ static function (PostSchema $schema, Post $post) { /** * @param Closure $scenario * @return void - * @dataProvider scenarioProvider + * @dataProvider relationshipProvider */ - public function testRelated(Closure $scenario): void + public function testRelationship(Closure $scenario): void { $expected = $scenario($this->schema, $this->post); $response = $this ->withoutExceptionHandling() ->jsonApi('users') - ->get(url('/api/v1/posts', [$this->post, 'author'])); + ->get(url('/api/v1/posts', [$this->post, 'relationships', 'author'])); - $response->assertFetchedOne($this->post->author); + $response->assertFetchedToOne($this->post->author); if (is_array($expected)) { $response->assertLinks($expected); } } + /** + * @return array[] + */ + public static function relatedProvider(): array + { + return [ + 'hidden' => [ + static function (PostSchema $schema) { + $schema->relationship('author')->hidden(); + return null; + }, + ], + 'no links' => [ + static function (PostSchema $schema) { + $schema->relationship('author')->serializeUsing( + static fn($relation) => $relation->withoutLinks() + ); + return null; + }, + ], + 'no self link' => [ + static function (PostSchema $schema, Post $post) { + $schema->relationship('author')->serializeUsing( + static fn($relation) => $relation->withoutSelfLink() + ); + // related becomes self + return ['self' => url('/api/v1/posts', [$post, 'author'])]; + }, + ], + 'no related link' => [ + static function (PostSchema $schema, Post $post) { + $schema->relationship('author')->serializeUsing( + static fn($relation) => $relation->withoutRelatedLink() + ); + // related becomes self but it's missing + return null; + }, + ], + ]; + } + /** * @param Closure $scenario * @return void - * @dataProvider scenarioProvider + * @dataProvider relatedProvider */ - public function testSelf(Closure $scenario): void + public function testRelated(Closure $scenario): void { $expected = $scenario($this->schema, $this->post); $response = $this ->withoutExceptionHandling() ->jsonApi('users') - ->get(url('/api/v1/posts', [$this->post, 'relationships', 'author'])); + ->get(url('/api/v1/posts', [$this->post, 'author'])); - $response->assertFetchedToOne($this->post->author); + $response->assertFetchedOne($this->post->author); if (is_array($expected)) { $response->assertLinks($expected); From e2b76002613a7dc84d595e190b9eb18a51ee8f9e Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Wed, 26 Jun 2024 20:36:10 +0100 Subject: [PATCH 02/18] feat: support Eloquent dynamic relationships --- CHANGELOG.md | 1 + composer.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9684c08..df35b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec - [core#17](https://github.com/laravel-json-api/core/pull/17) Fix incorrect `self` link in related resource responses, and remove `related` link that should not exist. This has been incorrect for some time, but is definitely what the [spec defines here.](https://jsonapi.org/format/1.0/#document-top-level) +- [eloquent#36](https://github.com/laravel-json-api/eloquent/pull/36) Support Eloquent dynamic relationships. ## [4.0.0] - 2024-03-14 diff --git a/composer.json b/composer.json index 3eba78d..9fb3e7a 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "php": "^8.2", "ext-json": "*", "laravel-json-api/core": "^4.1", - "laravel-json-api/eloquent": "^4.0", + "laravel-json-api/eloquent": "^4.1", "laravel-json-api/encoder-neomerx": "^4.0", "laravel-json-api/exceptions": "^3.0", "laravel-json-api/spec": "^3.0", From b2e1012fc097096a293a874aba301ca7ab43ea5c Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Wed, 26 Jun 2024 20:46:50 +0100 Subject: [PATCH 03/18] docs: update changelog and bump version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df35b11..ac5a964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [4.1.0] - 2024-06-26 + ### Fixed - [core#17](https://github.com/laravel-json-api/core/pull/17) Fix incorrect `self` link in related resource responses, From 2f0c672d336832f032b276578b44fc8f4888dd77 Mon Sep 17 00:00:00 2001 From: SerhiiKotelnikov <165281614+SerhiiKotelnikov@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:32:52 +0200 Subject: [PATCH 04/18] docs: update readme link to docs (#297) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50ef5fa..977e3d9 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ See our website, [laraveljsonapi.io](https://laraveljsonapi.io) ### Tutorial New to JSON:API and/or Laravel JSON:API? Then -the [Laravel JSON:API tutorial](https://laraveljsonapi.io/docs/2.0/tutorial/) +the [Laravel JSON:API tutorial](https://laraveljsonapi.io/4.x/tutorial/) is a great way to learn! Follow the tutorial to build a blog application with a JSON:API compliant API. From 0f10886a27d7d71a1240c7f7dc182c1bb3c3abbe Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Fri, 29 Nov 2024 18:32:02 +0000 Subject: [PATCH 05/18] fix: remove deprecation notices in php 8.4 --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 4 ++++ composer.json | 14 ++++++------- phpunit.xml | 21 ++++++++++++++----- src/Exceptions/HttpNotAcceptableException.php | 4 ++-- src/Routing/ActionRegistrar.php | 16 +++++++------- src/Routing/PendingResourceRegistration.php | 2 +- src/Routing/ResourceRegistrar.php | 2 +- tests/lib/Integration/Routing/TestCase.php | 6 +++--- 9 files changed, 43 insertions(+), 28 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 843531a..1f1b5a4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3] + php: [8.2, 8.3, 8.4] laravel: [11] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5a964..c713820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +### Fixed + +- Remove deprecation notices in PHP 8.4. + ## [4.1.0] - 2024-06-26 ### Fixed diff --git a/composer.json b/composer.json index 9fb3e7a..5ac9733 100644 --- a/composer.json +++ b/composer.json @@ -25,16 +25,16 @@ "require": { "php": "^8.2", "ext-json": "*", - "laravel-json-api/core": "^4.1", - "laravel-json-api/eloquent": "^4.1", - "laravel-json-api/encoder-neomerx": "^4.0", - "laravel-json-api/exceptions": "^3.0", - "laravel-json-api/spec": "^3.0", - "laravel-json-api/validation": "^4.0", + "laravel-json-api/core": "^4.3.2", + "laravel-json-api/eloquent": "^4.4", + "laravel-json-api/encoder-neomerx": "^4.1", + "laravel-json-api/exceptions": "^3.1", + "laravel-json-api/spec": "^3.1", + "laravel-json-api/validation": "^4.2", "laravel/framework": "^11.0" }, "require-dev": { - "laravel-json-api/testing": "^3.0", + "laravel-json-api/testing": "^3.0.2", "orchestra/testbench": "^9.0", "phpunit/phpunit": "^10.5" }, diff --git a/phpunit.xml b/phpunit.xml index 539875b..f2ba358 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,20 @@ - + diff --git a/src/Exceptions/HttpNotAcceptableException.php b/src/Exceptions/HttpNotAcceptableException.php index ea6384d..7f7594a 100644 --- a/src/Exceptions/HttpNotAcceptableException.php +++ b/src/Exceptions/HttpNotAcceptableException.php @@ -26,8 +26,8 @@ class HttpNotAcceptableException extends HttpException * @param int $code */ public function __construct( - string $message = null, - Throwable $previous = null, + ?string $message = null, + ?Throwable $previous = null, array $headers = [], int $code = 0 ) { diff --git a/src/Routing/ActionRegistrar.php b/src/Routing/ActionRegistrar.php index c259278..b98680f 100644 --- a/src/Routing/ActionRegistrar.php +++ b/src/Routing/ActionRegistrar.php @@ -77,7 +77,7 @@ public function __construct( string $resourceType, array $options, string $controller, - string $prefix = null + ?string $prefix = null ) { $this->router = $router; $this->resource = $resource; @@ -106,7 +106,7 @@ public function withId(): self * @param string|null $method * @return ActionProxy */ - public function get(string $uri, string $method = null): ActionProxy + public function get(string $uri, ?string $method = null): ActionProxy { return $this->register('get', $uri, $method); } @@ -118,7 +118,7 @@ public function get(string $uri, string $method = null): ActionProxy * @param string|null $method * @return ActionProxy */ - public function post(string $uri, string $method = null): ActionProxy + public function post(string $uri, ?string $method = null): ActionProxy { return $this->register('post', $uri, $method); } @@ -130,7 +130,7 @@ public function post(string $uri, string $method = null): ActionProxy * @param string|null $method * @return ActionProxy */ - public function patch(string $uri, string $method = null): ActionProxy + public function patch(string $uri, ?string $method = null): ActionProxy { return $this->register('patch', $uri, $method); } @@ -142,7 +142,7 @@ public function patch(string $uri, string $method = null): ActionProxy * @param string|null $method * @return ActionProxy */ - public function put(string $uri, string $method = null): ActionProxy + public function put(string $uri, ?string $method = null): ActionProxy { return $this->register('put', $uri, $method); } @@ -154,7 +154,7 @@ public function put(string $uri, string $method = null): ActionProxy * @param string|null $method * @return ActionProxy */ - public function delete(string $uri, string $method = null): ActionProxy + public function delete(string $uri, ?string $method = null): ActionProxy { return $this->register('delete', $uri, $method); } @@ -166,7 +166,7 @@ public function delete(string $uri, string $method = null): ActionProxy * @param string|null $method * @return ActionProxy */ - public function options(string $uri, string $method = null): ActionProxy + public function options(string $uri, ?string $method = null): ActionProxy { return $this->register('options', $uri, $method); } @@ -177,7 +177,7 @@ public function options(string $uri, string $method = null): ActionProxy * @param string|null $action * @return ActionProxy */ - public function register(string $method, string $uri, string $action = null): ActionProxy + public function register(string $method, string $uri, ?string $action = null): ActionProxy { $action = $action ?: $this->guessControllerAction($uri); $parameter = $this->getParameter(); diff --git a/src/Routing/PendingResourceRegistration.php b/src/Routing/PendingResourceRegistration.php index c98fa02..627bd0f 100644 --- a/src/Routing/PendingResourceRegistration.php +++ b/src/Routing/PendingResourceRegistration.php @@ -229,7 +229,7 @@ public function relationships(Closure $callback): self * @param Closure|null $callback * @return $this */ - public function actions($prefixOrCallback, Closure $callback = null): self + public function actions($prefixOrCallback, ?Closure $callback = null): self { if ($prefixOrCallback instanceof Closure && null === $callback) { $this->actionsPrefix = null; diff --git a/src/Routing/ResourceRegistrar.php b/src/Routing/ResourceRegistrar.php index 265fc79..5da31db 100644 --- a/src/Routing/ResourceRegistrar.php +++ b/src/Routing/ResourceRegistrar.php @@ -51,7 +51,7 @@ public function __construct(RegistrarContract $router, Server $server) * @param string|null $controller * @return PendingResourceRegistration */ - public function resource(string $resourceType, string $controller = null): PendingResourceRegistration + public function resource(string $resourceType, ?string $controller = null): PendingResourceRegistration { return new PendingResourceRegistration( $this, diff --git a/tests/lib/Integration/Routing/TestCase.php b/tests/lib/Integration/Routing/TestCase.php index e520ec1..d8d589f 100644 --- a/tests/lib/Integration/Routing/TestCase.php +++ b/tests/lib/Integration/Routing/TestCase.php @@ -66,8 +66,8 @@ protected function createServer(string $name): Server protected function createSchema( Server $server, string $name, - string $pattern = null, - string $uriType = null + ?string $pattern = null, + ?string $uriType = null ): Schema { $schema = $this->createMock(Schema::class); @@ -89,7 +89,7 @@ protected function createSchema( * @param string|null $uriName * @return void */ - protected function createRelation(MockObject $schema, string $fieldName, string $uriName = null): void + protected function createRelation(MockObject $schema, string $fieldName, ?string $uriName = null): void { $relation = $this->createMock(Relation::class); $relation->method('name')->willReturn($fieldName); From fa0addc1b47fff14d02259c38f4b52824d7d392d Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sat, 30 Nov 2024 17:58:48 +0000 Subject: [PATCH 06/18] docs: update changelog and bump version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c713820..f404766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [4.1.1] - 2024-11-30 + ### Fixed - Remove deprecation notices in PHP 8.4. From e8d4e193b0c3f160976ee29fc8305675def8c134 Mon Sep 17 00:00:00 2001 From: Gregory Haddow Date: Fri, 22 Nov 2024 10:58:23 +0000 Subject: [PATCH 07/18] feat: support auth responses from authorizer contract --- composer.json | 2 +- src/Http/Requests/FormRequest.php | 62 ++++++++++++------- src/Http/Requests/ResourceQuery.php | 5 +- src/Http/Requests/ResourceRequest.php | 5 +- .../Controllers/Api/V1/UserController.php | 1 + tests/dummy/app/Policies/UserPolicy.php | 14 +++++ tests/dummy/routes/api.php | 2 +- tests/dummy/tests/Api/V1/Users/DeleteTest.php | 38 ++++++++++++ 8 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 tests/dummy/tests/Api/V1/Users/DeleteTest.php diff --git a/composer.json b/composer.json index 5ac9733..7516ea9 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^8.2", "ext-json": "*", - "laravel-json-api/core": "^4.3.2", + "laravel-json-api/core": "^4.3.2|^5.0.1", "laravel-json-api/eloquent": "^4.4", "laravel-json-api/encoder-neomerx": "^4.1", "laravel-json-api/exceptions": "^3.1", diff --git a/src/Http/Requests/FormRequest.php b/src/Http/Requests/FormRequest.php index 1ff4f8e..a4974ca 100644 --- a/src/Http/Requests/FormRequest.php +++ b/src/Http/Requests/FormRequest.php @@ -11,6 +11,8 @@ namespace LaravelJsonApi\Laravel\Http\Requests; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\Access\Response; use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Auth\Guard; use Illuminate\Foundation\Http\FormRequest as BaseFormRequest; @@ -226,42 +228,54 @@ public function schema(): Schema */ protected function passesAuthorization() { - /** - * If the developer has implemented the `authorize` method, we - * will return the result if it is a boolean. This allows - * the developer to return a null value to indicate they want - * the default authorization to run. - */ - if (method_exists($this, 'authorize')) { - if (is_bool($passes = $this->container->call([$this, 'authorize']))) { - return $passes; + try { + /** + * If the developer has implemented the `authorize` method, we + * will return the result if it is a boolean. This allows + * the developer to return a null value to indicate they want + * the default authorization to run. + */ + if (method_exists($this, 'authorize')) { + $result = $this->container->call([$this, 'authorize']); + if ($result !== null) { + return $result instanceof Response ? $result->authorize() : $result; + } } - } - /** - * If the developer has not authorized the request themselves, - * we run our default authorization as long as authorization is - * enabled for both the server and the schema (checked via the - * `mustAuthorize()` method). - */ - if (method_exists($this, 'authorizeResource')) { - return $this->container->call([$this, 'authorizeResource']); - } + /** + * If the developer has not authorized the request themselves, + * we run our default authorization as long as authorization is + * enabled for both the server and the schema (checked via the + * `mustAuthorize()` method). + */ + if (method_exists($this, 'authorizeResource')) { + $result = $this->container->call([$this, 'authorizeResource']); + return $result instanceof Response ? $result->authorize() : $result; + } + } catch (AuthorizationException $ex) { + $this->failIfUnauthenticated(); + throw $ex; + } return true; } - /** - * @inheritDoc - */ - protected function failedAuthorization() + protected function failIfUnauthenticated() { - /** @var Guard $auth */ + /** @var Guard $auth */ $auth = $this->container->make(Guard::class); if ($auth->guest()) { throw new AuthenticationException(); } + } + + /** + * @inheritDoc + */ + protected function failedAuthorization() + { + $this->failIfUnauthenticated(); parent::failedAuthorization(); } diff --git a/src/Http/Requests/ResourceQuery.php b/src/Http/Requests/ResourceQuery.php index 116d5b9..940fc6e 100644 --- a/src/Http/Requests/ResourceQuery.php +++ b/src/Http/Requests/ResourceQuery.php @@ -11,6 +11,7 @@ namespace LaravelJsonApi\Laravel\Http\Requests; +use Illuminate\Auth\Access\Response; use Illuminate\Contracts\Validation\Validator; use Illuminate\Database\Eloquent\Model; use LaravelJsonApi\Contracts\Auth\Authorizer; @@ -104,9 +105,9 @@ public static function queryOne(string $resourceType): QueryParameters * Perform resource authorization. * * @param Authorizer $authorizer - * @return bool + * @return bool|Response */ - public function authorizeResource(Authorizer $authorizer): bool + public function authorizeResource(Authorizer $authorizer): bool|Response { if ($this->isViewingAny()) { return $authorizer->index( diff --git a/src/Http/Requests/ResourceRequest.php b/src/Http/Requests/ResourceRequest.php index 7a0a267..7b0ef36 100644 --- a/src/Http/Requests/ResourceRequest.php +++ b/src/Http/Requests/ResourceRequest.php @@ -11,6 +11,7 @@ namespace LaravelJsonApi\Laravel\Http\Requests; +use Illuminate\Auth\Access\Response; use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Illuminate\Contracts\Validation\Validator; use Illuminate\Database\Eloquent\Model; @@ -150,9 +151,9 @@ public function toMany(): Collection * Perform resource authorization. * * @param Authorizer $authorizer - * @return bool + * @return bool|Response */ - public function authorizeResource(Authorizer $authorizer): bool + public function authorizeResource(Authorizer $authorizer): bool|Response { if ($this->isCreating()) { return $authorizer->store( diff --git a/tests/dummy/app/Http/Controllers/Api/V1/UserController.php b/tests/dummy/app/Http/Controllers/Api/V1/UserController.php index 0975510..131460c 100644 --- a/tests/dummy/app/Http/Controllers/Api/V1/UserController.php +++ b/tests/dummy/app/Http/Controllers/Api/V1/UserController.php @@ -19,6 +19,7 @@ class UserController extends Controller { use Actions\FetchOne; + use Actions\Destroy; use Actions\FetchRelated; use Actions\FetchRelationship; use Actions\UpdateRelationship; diff --git a/tests/dummy/app/Policies/UserPolicy.php b/tests/dummy/app/Policies/UserPolicy.php index 6c3e233..6fc202c 100644 --- a/tests/dummy/app/Policies/UserPolicy.php +++ b/tests/dummy/app/Policies/UserPolicy.php @@ -12,6 +12,7 @@ namespace App\Policies; use App\Models\User; +use Illuminate\Auth\Access\Response; class UserPolicy { @@ -50,4 +51,17 @@ public function updatePhone(User $user, User $other): bool { return $user->is($other); } + + /** + * Determine if the user can delete the other user. + * + * @param User $user + * @param User $other + * @return bool|Response + */ + public function delete(User $user, User $other) + { + return $user->is($other) ? true : Response::denyAsNotFound('not found message'); + } + } diff --git a/tests/dummy/routes/api.php b/tests/dummy/routes/api.php index a5de0cb..482a64d 100644 --- a/tests/dummy/routes/api.php +++ b/tests/dummy/routes/api.php @@ -25,7 +25,7 @@ }); /** Users */ - $server->resource('users')->only('show')->relationships(function ($relationships) { + $server->resource('users')->only('show','destroy')->relationships(function ($relationships) { $relationships->hasOne('phone'); })->actions(function ($actions) { $actions->get('me'); diff --git a/tests/dummy/tests/Api/V1/Users/DeleteTest.php b/tests/dummy/tests/Api/V1/Users/DeleteTest.php new file mode 100644 index 0000000..4980767 --- /dev/null +++ b/tests/dummy/tests/Api/V1/Users/DeleteTest.php @@ -0,0 +1,38 @@ +createOne(); + + $expected = $this->serializer + ->user($user); + $response = $this + ->actingAs(User::factory()->createOne()) + ->jsonApi('users') + ->delete(url('/api/v1/users', $expected['id'])); + + $response->assertNotFound() + ->assertHasError(404, [ + 'detail' => 'not found message', + 'status' => '404', + 'title' => 'Not Found', + ]); + } +} From db53f2f69355180938166789198c6ae3aba84b44 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 1 Dec 2024 19:43:48 +0000 Subject: [PATCH 08/18] build!: update changelog, bump branch alias and prep for major release --- CHANGELOG.md | 10 ++++++++++ composer.json | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f404766..fa658b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +### Changed + +- [#298](https://github.com/laravel-json-api/laravel/pull/298) + and [#70](https://github.com/laravel-json-api/laravel/issues/70) The authorizer implementation now allows methods to + return either `bool` or an Illuminate Auth `Response`. +- **BREAKING** The return type for the `authorizeResource()` method on both resource and query request classes has + changed to `bool|Response` (where response is the Illuminate Auth response). If you are manually calling this method + and relying on the return value being a boolean, this change is breaking. However, the vast majority of applications + should be able to upgrade without any changes. + ## [4.1.1] - 2024-11-30 ### Fixed diff --git a/composer.json b/composer.json index 7516ea9..e6fb771 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^8.2", "ext-json": "*", - "laravel-json-api/core": "^4.3.2|^5.0.1", + "laravel-json-api/core": "^5.0.1", "laravel-json-api/eloquent": "^4.4", "laravel-json-api/encoder-neomerx": "^4.1", "laravel-json-api/exceptions": "^3.1", @@ -53,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "4.x-dev" + "dev-develop": "5.x-dev" }, "laravel": { "aliases": { From ee6e2f43aa80be57d63f293bdbcd4eb99dd3642d Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 1 Dec 2024 19:44:31 +0000 Subject: [PATCH 09/18] feat: update authorizer stub --- stubs/authorizer.stub | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/stubs/authorizer.stub b/stubs/authorizer.stub index 1cd9ba8..8fdc6c9 100644 --- a/stubs/authorizer.stub +++ b/stubs/authorizer.stub @@ -2,6 +2,7 @@ namespace {{ namespace }}; +use Illuminate\Auth\Access\Response; use Illuminate\Http\Request; use LaravelJsonApi\Contracts\Auth\Authorizer; @@ -13,9 +14,9 @@ class {{ class }} implements Authorizer * * @param Request $request * @param string $modelClass - * @return bool + * @return bool|Response */ - public function index(Request $request, string $modelClass): bool + public function index(Request $request, string $modelClass): bool|Response { // TODO: Implement index() method. } @@ -25,9 +26,9 @@ class {{ class }} implements Authorizer * * @param Request $request * @param string $modelClass - * @return bool + * @return bool|Response */ - public function store(Request $request, string $modelClass): bool + public function store(Request $request, string $modelClass): bool|Response { // TODO: Implement store() method. } @@ -37,9 +38,9 @@ class {{ class }} implements Authorizer * * @param Request $request * @param object $model - * @return bool + * @return bool|Response */ - public function show(Request $request, object $model): bool + public function show(Request $request, object $model): bool|Response { // TODO: Implement show() method. } @@ -49,9 +50,9 @@ class {{ class }} implements Authorizer * * @param object $model * @param Request $request - * @return bool + * @return bool|Response */ - public function update(Request $request, object $model): bool + public function update(Request $request, object $model): bool|Response { // TODO: Implement update() method. } @@ -61,9 +62,9 @@ class {{ class }} implements Authorizer * * @param Request $request * @param object $model - * @return bool + * @return bool|Response */ - public function destroy(Request $request, object $model): bool + public function destroy(Request $request, object $model): bool|Response { // TODO: Implement destroy() method. } @@ -74,9 +75,9 @@ class {{ class }} implements Authorizer * @param Request $request * @param object $model * @param string $fieldName - * @return bool + * @return bool|Response */ - public function showRelated(Request $request, object $model, string $fieldName): bool + public function showRelated(Request $request, object $model, string $fieldName): bool|Response { // TODO: Implement showRelated() method. } @@ -87,9 +88,9 @@ class {{ class }} implements Authorizer * @param Request $request * @param object $model * @param string $fieldName - * @return bool + * @return bool|Response */ - public function showRelationship(Request $request, object $model, string $fieldName): bool + public function showRelationship(Request $request, object $model, string $fieldName): bool|Response { // TODO: Implement showRelationship() method. } @@ -100,9 +101,9 @@ class {{ class }} implements Authorizer * @param Request $request * @param object $model * @param string $fieldName - * @return bool + * @return bool|Response */ - public function updateRelationship(Request $request, object $model, string $fieldName): bool + public function updateRelationship(Request $request, object $model, string $fieldName): bool|Response { // TODO: Implement updateRelationship() method. } @@ -113,9 +114,9 @@ class {{ class }} implements Authorizer * @param Request $request * @param object $model * @param string $fieldName - * @return bool + * @return bool|Response */ - public function attachRelationship(Request $request, object $model, string $fieldName): bool + public function attachRelationship(Request $request, object $model, string $fieldName): bool|Response { // TODO: Implement attachRelationship() method. } @@ -126,9 +127,9 @@ class {{ class }} implements Authorizer * @param Request $request * @param object $model * @param string $fieldName - * @return bool + * @return bool|Response */ - public function detachRelationship(Request $request, object $model, string $fieldName): bool + public function detachRelationship(Request $request, object $model, string $fieldName): bool|Response { // TODO: Implement detachRelationship() method. } From b2edf37c27bcead77b90cddcbb6f49a1491a68d6 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 1 Dec 2024 19:48:47 +0000 Subject: [PATCH 10/18] docs: update changelog and bump version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa658b6..0a97dc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [5.0.0] - 2025-12-01 + ### Changed - [#298](https://github.com/laravel-json-api/laravel/pull/298) From 39db9638bf79ad8bb0f87a7b680a665bba7536e2 Mon Sep 17 00:00:00 2001 From: Gregory Haddow Date: Mon, 2 Dec 2024 11:15:39 +0000 Subject: [PATCH 11/18] fix: authorizer response with status should be honoured when unauthenticated --- src/Http/Requests/FormRequest.php | 4 +++- tests/dummy/app/Policies/UserPolicy.php | 6 +++--- tests/dummy/tests/Api/V1/Users/DeleteTest.php | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Http/Requests/FormRequest.php b/src/Http/Requests/FormRequest.php index a4974ca..928b489 100644 --- a/src/Http/Requests/FormRequest.php +++ b/src/Http/Requests/FormRequest.php @@ -254,7 +254,9 @@ protected function passesAuthorization() } } catch (AuthorizationException $ex) { - $this->failIfUnauthenticated(); + if (!$ex->hasStatus()) { + $this->failIfUnauthenticated(); + } throw $ex; } return true; diff --git a/tests/dummy/app/Policies/UserPolicy.php b/tests/dummy/app/Policies/UserPolicy.php index 6fc202c..c2b6224 100644 --- a/tests/dummy/app/Policies/UserPolicy.php +++ b/tests/dummy/app/Policies/UserPolicy.php @@ -55,13 +55,13 @@ public function updatePhone(User $user, User $other): bool /** * Determine if the user can delete the other user. * - * @param User $user + * @param ?User $user * @param User $other * @return bool|Response */ - public function delete(User $user, User $other) + public function delete(?User $user, User $other) { - return $user->is($other) ? true : Response::denyAsNotFound('not found message'); + return $user?->is($other) ? true : Response::denyAsNotFound('not found message'); } } diff --git a/tests/dummy/tests/Api/V1/Users/DeleteTest.php b/tests/dummy/tests/Api/V1/Users/DeleteTest.php index 4980767..6679084 100644 --- a/tests/dummy/tests/Api/V1/Users/DeleteTest.php +++ b/tests/dummy/tests/Api/V1/Users/DeleteTest.php @@ -35,4 +35,22 @@ public function test(): void 'title' => 'Not Found', ]); } + + public function testUnauthenticated(): void + { + $user = User::factory()->createOne(); + + $expected = $this->serializer + ->user($user); + $response = $this + ->jsonApi('users') + ->delete(url('/api/v1/users', $expected['id'])); + + $response->assertNotFound() + ->assertHasError(404, [ + 'detail' => 'not found message', + 'status' => '404', + 'title' => 'Not Found', + ]); + } } From d2815a700ea8f79d15ab965cf00d5ca8e0450022 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Mon, 2 Dec 2024 17:26:51 +0000 Subject: [PATCH 12/18] tests: tidy up user delete test --- tests/dummy/tests/Api/V1/Users/DeleteTest.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/dummy/tests/Api/V1/Users/DeleteTest.php b/tests/dummy/tests/Api/V1/Users/DeleteTest.php index 6679084..135ea71 100644 --- a/tests/dummy/tests/Api/V1/Users/DeleteTest.php +++ b/tests/dummy/tests/Api/V1/Users/DeleteTest.php @@ -16,20 +16,16 @@ class DeleteTest extends TestCase { - public function test(): void { $user = User::factory()->createOne(); - $expected = $this->serializer - ->user($user); $response = $this ->actingAs(User::factory()->createOne()) ->jsonApi('users') - ->delete(url('/api/v1/users', $expected['id'])); + ->delete(url('/api/v1/users', $user)); - $response->assertNotFound() - ->assertHasError(404, [ + $response->assertNotFound()->assertErrorStatus([ 'detail' => 'not found message', 'status' => '404', 'title' => 'Not Found', @@ -40,14 +36,11 @@ public function testUnauthenticated(): void { $user = User::factory()->createOne(); - $expected = $this->serializer - ->user($user); $response = $this ->jsonApi('users') - ->delete(url('/api/v1/users', $expected['id'])); + ->delete(url('/api/v1/users', $user)); - $response->assertNotFound() - ->assertHasError(404, [ + $response->assertNotFound()->assertErrorStatus([ 'detail' => 'not found message', 'status' => '404', 'title' => 'Not Found', From fd0bf659cd035457aaf061c9701471bff77cc50a Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Mon, 2 Dec 2024 17:28:21 +0000 Subject: [PATCH 13/18] docs: update changelog and bump version --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a97dc8..f6912ac 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 +## [5.0.1] - 2025-12-02 + +### Fixed + +- [#301](https://github.com/laravel-json-api/laravel/pull/301) Do not override response status when authorization + exception is thrown. + ## [5.0.0] - 2025-12-01 ### Changed From f84bd21051a81f1ad06194abc7c0a9e0dec2cd84 Mon Sep 17 00:00:00 2001 From: Gregory Haddow Date: Tue, 3 Dec 2024 20:29:51 +0000 Subject: [PATCH 14/18] fix: authorizer response should be honoured on destroy action when no request class for resource --- src/Http/Controllers/Actions/Destroy.php | 18 +++++-- tests/dummy/app/Policies/TagPolicy.php | 25 ++++++++++ tests/dummy/routes/api.php | 3 ++ tests/dummy/tests/Api/V1/Tags/DeleteTest.php | 50 ++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/dummy/app/Policies/TagPolicy.php create mode 100644 tests/dummy/tests/Api/V1/Tags/DeleteTest.php diff --git a/src/Http/Controllers/Actions/Destroy.php b/src/Http/Controllers/Actions/Destroy.php index 8ab981b..6e85a1b 100644 --- a/src/Http/Controllers/Actions/Destroy.php +++ b/src/Http/Controllers/Actions/Destroy.php @@ -12,6 +12,7 @@ namespace LaravelJsonApi\Laravel\Http\Controllers\Actions; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\Access\Response as AuthResponse; use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\Response; @@ -63,13 +64,24 @@ public function destroy(Route $route, StoreContract $store) * So we need to trigger authorization in this case. */ if (!$request) { - $check = $route->authorizer()->destroy( + $result = $route->authorizer()->destroy( $request = \request(), $model, ); - throw_if(false === $check && Auth::guest(), new AuthenticationException()); - throw_if(false === $check, new AuthorizationException()); + if ($result instanceof AuthResponse) { + try { + $result->authorize(); + } catch (AuthorizationException $ex) { + if (!$ex->hasStatus()) { + throw_if(Auth::guest(), new AuthenticationException()); + } + throw $ex; + } + } + + throw_if(false === $result && Auth::guest(), new AuthenticationException()); + throw_if(false === $result, new AuthorizationException()); } $response = null; diff --git a/tests/dummy/app/Policies/TagPolicy.php b/tests/dummy/app/Policies/TagPolicy.php new file mode 100644 index 0000000..ff13681 --- /dev/null +++ b/tests/dummy/app/Policies/TagPolicy.php @@ -0,0 +1,25 @@ +prefix('v1') @@ -35,4 +36,6 @@ $server->resource('videos')->relationships(function ($relationships) { $relationships->hasMany('tags'); }); + + $server->resource('tags', '\\' . JsonApiController::class)->only('destroy'); }); diff --git a/tests/dummy/tests/Api/V1/Tags/DeleteTest.php b/tests/dummy/tests/Api/V1/Tags/DeleteTest.php new file mode 100644 index 0000000..ebb5460 --- /dev/null +++ b/tests/dummy/tests/Api/V1/Tags/DeleteTest.php @@ -0,0 +1,50 @@ +createOne(); + + $response = $this + ->actingAs(User::factory()->createOne()) + ->jsonApi('users') + ->delete(url('/api/v1/tags', $tag)); + + $response->assertNotFound()->assertErrorStatus([ + 'detail' => 'not found message', + 'status' => '404', + 'title' => 'Not Found', + ]); + } + + public function testUnauthenticated(): void + { + $tag = Tag::factory()->createOne(); + + $response = $this + ->jsonApi('users') + ->delete(url('/api/v1/tags', $tag)); + + $response->assertNotFound()->assertErrorStatus([ + 'detail' => 'not found message', + 'status' => '404', + 'title' => 'Not Found', + ]); + } +} From 87442c6426b854a33389e34faebf730256494c48 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Tue, 3 Dec 2024 20:42:52 +0000 Subject: [PATCH 15/18] docs: update changelog and bump version --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6912ac..bd4fa13 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 +## [5.0.2] - 2025-12-03 + +### Fixed + +- [#302](https://github.com/laravel-json-api/laravel/pull/302) Ensure auth response is used when deleting a resource + that does not have a resource response class. + ## [5.0.1] - 2025-12-02 ### Fixed From 3456717d116c375c25276a8811e2d35447937525 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Mon, 24 Feb 2025 20:18:02 +0000 Subject: [PATCH 16/18] feat: add support for Laravel 12 --- .github/workflows/tests.yml | 14 ++++++++++---- composer.json | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f1b5a4..d9d6048 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,15 @@ name: Tests on: push: - branches: [ main, develop, 3.x ] + branches: + - main + - develop + - 3.x pull_request: - branches: [ main, develop, 3.x ] + branches: + - main + - develop + - 3.x jobs: build: @@ -14,8 +20,8 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3, 8.4] - laravel: [11] + php: [ 8.2, 8.3, 8.4 ] + laravel: [ 11, 12 ] steps: - name: Checkout Code diff --git a/composer.json b/composer.json index e6fb771..4f187ca 100644 --- a/composer.json +++ b/composer.json @@ -25,18 +25,18 @@ "require": { "php": "^8.2", "ext-json": "*", - "laravel-json-api/core": "^5.0.1", - "laravel-json-api/eloquent": "^4.4", - "laravel-json-api/encoder-neomerx": "^4.1", - "laravel-json-api/exceptions": "^3.1", - "laravel-json-api/spec": "^3.1", - "laravel-json-api/validation": "^4.2", - "laravel/framework": "^11.0" + "laravel-json-api/core": "^5.2", + "laravel-json-api/eloquent": "^4.5", + "laravel-json-api/encoder-neomerx": "^4.2", + "laravel-json-api/exceptions": "^3.2", + "laravel-json-api/spec": "^3.2", + "laravel-json-api/validation": "^4.3", + "laravel/framework": "^11.0|^12.0" }, "require-dev": { - "laravel-json-api/testing": "^3.0.2", - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" + "laravel-json-api/testing": "^3.1", + "orchestra/testbench": "^9.0|^10.0", + "phpunit/phpunit": "^10.5|^11.0" }, "autoload": { "psr-4": { @@ -65,7 +65,7 @@ ] } }, - "minimum-stability": "stable", + "minimum-stability": "dev", "prefer-stable": true, "config": { "sort-packages": true From 9ec6f44b10373bcd735b00d6521eb07222906f98 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Mon, 24 Feb 2025 20:52:41 +0000 Subject: [PATCH 17/18] build(deps): update to stable dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f187ca..a91b5fb 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ ] } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true, "config": { "sort-packages": true From 1d9955f56c5d142ba87582b342afe18b96e76bbf Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Mon, 24 Feb 2025 20:53:37 +0000 Subject: [PATCH 18/18] docs: update changelog and bump version --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4fa13..5b87944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [5.1.0] - 2025-02-24 + +### Added + +- Package now supports Laravel 12. + ## [5.0.2] - 2025-12-03 ### Fixed