diff --git a/framework/core/src/Api/Endpoint/Endpoint.php b/framework/core/src/Api/Endpoint/Endpoint.php index 9250f02d61..2b33e615cc 100644 --- a/framework/core/src/Api/Endpoint/Endpoint.php +++ b/framework/core/src/Api/Endpoint/Endpoint.php @@ -148,6 +148,10 @@ public function handle(\Tobyz\JsonApiServer\Context $context): ?Response return json_api_response($this->showResource($context, $data)); } + if (is_array($data)) { + return json_api_response($data); + } + return null; } } diff --git a/framework/core/src/Extend/ApiResource.php b/framework/core/src/Extend/ApiResource.php index 2c6634e112..71cce699cb 100644 --- a/framework/core/src/Extend/ApiResource.php +++ b/framework/core/src/Extend/ApiResource.php @@ -22,6 +22,8 @@ class ApiResource implements ExtenderInterface { private array $endpoints = []; + private array $endpointsBefore = []; + private array $endpointsAfter = []; private array $removeEndpoints = []; private array $endpoint = []; private array $fields = []; @@ -55,6 +57,45 @@ public function endpoints(callable|string $endpoints): self return $this; } + /** + * Add endpoints to the resource before a certain endpoint. + * + * @param string $before the name of the endpoint to add the new endpoints before. + * @param callable|class-string $endpoints must be a callable that returns an array of objects that implement \Flarum\Api\Endpoint\Endpoint. + */ + public function endpointsBefore(string $before, callable|string $endpoints): self + { + $this->endpointsBefore[] = [$before, $endpoints]; + + return $this; + } + + /** + * Add endpoints to the resource after a certain endpoint. + * + * @param string $after the name of the endpoint to add the new endpoints after. + * @param callable|class-string $endpoints must be a callable that returns an array of objects that implement \Flarum\Api\Endpoint\Endpoint. + */ + public function endpointsAfter(string $after, callable|string $endpoints): self + { + $this->endpointsAfter[] = [$after, $endpoints]; + + return $this; + } + + /** + * Add endpoints to the resource before all other endpoints. + * + * @param string $after the name of the endpoint to add the new endpoints after. + * @param callable|class-string $endpoints must be a callable that returns an array of objects that implement \Flarum\Api\Endpoint\Endpoint. + */ + public function endpointsBeforeAll(callable|string $endpoints): self + { + $this->endpointsBefore[] = [0, $endpoints]; + + return $this; + } + /** * Remove endpoints from the resource. * @@ -214,6 +255,31 @@ function (array $endpoints, Resource $resource) use ($container): array { $endpoints = array_merge($endpoints, $newEndpointsCallback()); } + foreach ($this->endpointsBefore as [$before, $newEndpointsCallback]) { + $newEndpointsCallback = ContainerUtil::wrapCallback($newEndpointsCallback, $container); + + if ($before === 0) { + array_unshift($endpoints, ...$newEndpointsCallback()); + } else { + $newEndpoints = $newEndpointsCallback(); + $beforeIndex = array_search($before, array_column($endpoints, 'name')); + + if ($beforeIndex !== false) { + array_splice($endpoints, $beforeIndex, 0, $newEndpoints); + } + } + } + + foreach ($this->endpointsAfter as [$after, $newEndpointsCallback]) { + $newEndpointsCallback = ContainerUtil::wrapCallback($newEndpointsCallback, $container); + $newEndpoints = $newEndpointsCallback(); + $afterIndex = array_search($after, array_column($endpoints, 'name')); + + if ($afterIndex !== false) { + array_splice($endpoints, $afterIndex + 1, 0, $newEndpoints); + } + } + foreach ($this->removeEndpoints as $removeEndpointClass) { [$endpointsToRemove, $condition] = $removeEndpointClass; diff --git a/framework/core/tests/integration/extenders/ApiResourceTest.php b/framework/core/tests/integration/extenders/ApiResourceTest.php index 088bc51e58..8935d0abad 100644 --- a/framework/core/tests/integration/extenders/ApiResourceTest.php +++ b/framework/core/tests/integration/extenders/ApiResourceTest.php @@ -11,6 +11,7 @@ use Carbon\Carbon; use Flarum\Api\Context; +use Flarum\Api\Endpoint\Endpoint; use Flarum\Api\Endpoint\Index; use Flarum\Api\Endpoint\Show; use Flarum\Api\Resource\AbstractDatabaseResource; @@ -91,6 +92,66 @@ public function after_endpoint_callback_works_if_added() $this->assertEquals('dataSerializationPrepCustomTitle', $payload['data']['attributes']['title'], $body); } + #[Test] + public function custom_endpoint_works_if_added() + { + $this->extend( + (new Extend\ApiResource(DiscussionResource::class)) + ->endpoints(fn () => [ + Endpoint::make('custom') + ->route('GET', '/{id}/custom') + ->action(function (Context $context) { + $discussion = $context->model; + + return [ + 'data' => [ + 'message' => 'custom endpoint '.$discussion->id, + ], + ]; + }), + ]) + ); + + $response = $this->send( + $this->request('GET', '/api/discussions/1/custom', [ + 'authenticatedAs' => 1, + ]) + ); + + $payload = json_decode($body = $response->getBody()->getContents(), true); + + $this->assertEquals('custom endpoint 1', $payload['data']['message'], $body); + } + + #[Test] + public function custom_endpoint_works_if_added_before_all() + { + $this->extend( + (new Extend\ApiResource(DiscussionResource::class)) + ->endpointsBeforeAll(fn () => [ + Endpoint::make('custom') + ->route('GET', '/custom') + ->action(function (Context $context) { + return [ + 'data' => [ + 'message' => 'custom endpoint', + ], + ]; + }), + ]) + ); + + $response = $this->send( + $this->request('GET', '/api/discussions/custom', [ + 'authenticatedAs' => 1, + ]) + ); + + $payload = json_decode($body = $response->getBody()->getContents(), true); + + $this->assertEquals('custom endpoint', $payload['data']['message'], $body); + } + #[Test] public function after_endpoint_callback_works_with_invokable_classes() {