From aa39d0c11b26c779d9a9e59672d51bbc9b0431a5 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 10 May 2024 20:29:44 +0100 Subject: [PATCH] chore: adapt --- extensions/likes/extend.php | 4 +- extensions/mentions/extend.php | 4 +- framework/core/src/Api/ApiServiceProvider.php | 5 +- framework/core/src/Api/Context.php | 8 +- .../Concerns/ExtractsListingParams.php | 8 +- .../Api/Endpoint/Concerns/HasEagerLoading.php | 3 +- .../Concerns/SavesAndValidatesData.php | 8 +- framework/core/src/Api/Endpoint/Create.php | 6 +- framework/core/src/Api/Endpoint/Delete.php | 8 +- framework/core/src/Api/Endpoint/Endpoint.php | 8 +- .../src/Api/Endpoint/EndpointInterface.php | 15 -- framework/core/src/Api/Endpoint/Index.php | 25 ++- framework/core/src/Api/Endpoint/Update.php | 8 +- framework/core/src/Api/JsonApi.php | 24 ++- .../Api/Resource/AbstractDatabaseResource.php | 159 +-------------- .../src/Api/Resource/AbstractResource.php | 3 +- .../src/Api/Resource/Concerns/HasHooks.php | 182 ++++++++++++++++++ .../src/Api/Resource/DiscussionResource.php | 7 +- .../core/src/Api/Resource/EloquentBuffer.php | 29 +-- .../core/src/Api/Resource/PostResource.php | 1 + .../Schema/Contracts/RelationAggregator.php | 4 +- framework/core/src/Extend/ApiResource.php | 9 +- php-packages/phpstan/extension.neon | 1 + php-packages/phpstan/phpstan-baseline.neon | 4 + .../stubs/Tobyz/JsonApiServer/Context.stub | 11 ++ 25 files changed, 323 insertions(+), 221 deletions(-) delete mode 100644 framework/core/src/Api/Endpoint/EndpointInterface.php create mode 100644 framework/core/src/Api/Resource/Concerns/HasHooks.php create mode 100644 php-packages/phpstan/stubs/Tobyz/JsonApiServer/Context.stub diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index 56c147dfe..e84f0c5a1 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -45,13 +45,13 @@ return [ ->fields(PostResourceFields::class) ->endpoint( [Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class], - function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\EndpointInterface { + function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint { return $endpoint->addDefaultInclude(['likes']); } ), (new Extend\ApiResource(Resource\DiscussionResource::class)) - ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\EndpointInterface { + ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint { return $endpoint->addDefaultInclude(['posts.likes']); }), diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 4ef314e48..6960bebfe 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -63,7 +63,7 @@ return [ (new Extend\ApiResource(Resource\PostResource::class)) ->fields(PostResourceFields::class) - ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface { + ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint { return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']); }) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index { @@ -137,7 +137,7 @@ return [ }), (new Extend\ApiResource(Resource\PostResource::class)) - ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface { + ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint { return $endpoint->eagerLoad(['mentionsTags']); }), ]), diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index b165e863b..f6fd1eab9 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -9,7 +9,7 @@ namespace Flarum\Api; -use Flarum\Api\Endpoint\EndpointInterface; +use Flarum\Api\Endpoint\Endpoint; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\ErrorHandling\JsonApiFormatter; use Flarum\Foundation\ErrorHandling\Registry; @@ -22,7 +22,6 @@ use Flarum\Http\UrlGenerator; use Illuminate\Contracts\Container\Container; use Laminas\Stratigility\MiddlewarePipe; use ReflectionClass; -use Tobyz\JsonApiServer\Endpoint\Endpoint; class ApiServiceProvider extends AbstractServiceProvider { @@ -199,7 +198,7 @@ class ApiServiceProvider extends AbstractServiceProvider * None of the injected dependencies should be directly used within * the `endpoints` method. Encourage using callbacks. * - * @var array $endpoints + * @var array $endpoints */ $endpoints = $resource->resolveEndpoints(true); diff --git a/framework/core/src/Api/Context.php b/framework/core/src/Api/Context.php index 7bbfc78d7..75b45cc6e 100644 --- a/framework/core/src/Api/Context.php +++ b/framework/core/src/Api/Context.php @@ -57,6 +57,7 @@ class Context extends BaseContext $fields = []; + // @phpstan-ignore-next-line foreach ($resource->resolveFields() as $field) { $fields[$field->name] = $field; } @@ -156,10 +157,13 @@ class Context extends BaseContext return $new; } - public function extractIdFromPath(\Tobyz\JsonApiServer\Context $context): ?string + public function extractIdFromPath(BaseContext $context): ?string { + /** @var Endpoint\Endpoint $endpoint */ + $endpoint = $context->endpoint; + $currentPath = trim($context->path(), '/'); - $path = trim($context->collection->name().$this->endpoint->path, '/'); + $path = trim($context->collection->name().$endpoint->path, '/'); if (! str_contains($path, '{id}')) { return null; diff --git a/framework/core/src/Api/Endpoint/Concerns/ExtractsListingParams.php b/framework/core/src/Api/Endpoint/Concerns/ExtractsListingParams.php index 1b7a44b83..046d01046 100644 --- a/framework/core/src/Api/Endpoint/Concerns/ExtractsListingParams.php +++ b/framework/core/src/Api/Endpoint/Concerns/ExtractsListingParams.php @@ -10,9 +10,9 @@ namespace Flarum\Api\Endpoint\Concerns; use Closure; +use Flarum\Api\Resource\AbstractResource; use Flarum\Http\RequestUtil; use Tobyz\JsonApiServer\Context; -use Tobyz\JsonApiServer\Resource\AbstractResource; use Tobyz\JsonApiServer\Schema\Sort; trait ExtractsListingParams @@ -110,11 +110,13 @@ trait ExtractsListingParams public function getAvailableSorts(Context $context): array { - if (! $context->collection instanceof AbstractResource) { + $collection = $context->collection; + + if (! $collection instanceof AbstractResource) { return []; } - $asc = collect($context->collection->resolveSorts()) + $asc = collect($collection->resolveSorts()) ->filter(fn (Sort $field) => $field->isVisible($context)) ->pluck('name') ->toArray(); diff --git a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php b/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php index 225dc7c3d..19c3bb4f8 100644 --- a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php +++ b/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php @@ -9,6 +9,7 @@ namespace Flarum\Api\Endpoint\Concerns; +use Closure; use Flarum\Api\Resource\AbstractDatabaseResource; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; @@ -36,7 +37,7 @@ trait HasEagerLoading * * @param string|string[] $relations */ - public function eagerLoad(array|string|callable $relations): static + public function eagerLoad(array|string|Closure $relations): static { if (! is_callable($relations)) { $this->loadRelations = array_merge($this->loadRelations, array_map('strval', (array) $relations)); diff --git a/framework/core/src/Api/Endpoint/Concerns/SavesAndValidatesData.php b/framework/core/src/Api/Endpoint/Concerns/SavesAndValidatesData.php index 10f439a97..2fa6a8f7c 100644 --- a/framework/core/src/Api/Endpoint/Concerns/SavesAndValidatesData.php +++ b/framework/core/src/Api/Endpoint/Concerns/SavesAndValidatesData.php @@ -9,6 +9,8 @@ namespace Flarum\Api\Endpoint\Concerns; +use Flarum\Api\Schema\Concerns\FlarumField; +use Flarum\Api\Schema\Concerns\HasValidationRules; use Illuminate\Contracts\Validation\Validator; use Illuminate\Translation\ArrayLoader; use Illuminate\Translation\Translator; @@ -30,6 +32,7 @@ trait SavesAndValidatesData /** * Assert that the field values within a data object pass validation. * + * @param \Flarum\Api\Context $context * @throws UnprocessableEntityException */ protected function assertDataValid(Context $context, array $data): void @@ -49,14 +52,17 @@ trait SavesAndValidatesData foreach ($context->fields($context->resource) as $field) { $writable = $field->isWritable($context->withField($field)); - if (! $writable) { + if (! $writable || ! in_array(HasValidationRules::class, class_uses_recursive($field))) { continue; } $type = $field instanceof Attribute ? 'attributes' : 'relationships'; + // @phpstan-ignore-next-line $rules[$type] = array_merge($rules[$type], $field->getValidationRules($context)); + // @phpstan-ignore-next-line $messages = array_merge($messages, $field->getValidationMessages($context)); + // @phpstan-ignore-next-line $attributes = array_merge($attributes, $field->getValidationAttributes($context)); } diff --git a/framework/core/src/Api/Endpoint/Create.php b/framework/core/src/Api/Endpoint/Create.php index 37fb67ea6..cdc3061fa 100644 --- a/framework/core/src/Api/Endpoint/Create.php +++ b/framework/core/src/Api/Endpoint/Create.php @@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\IncludesData; use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData; use Flarum\Api\Endpoint\Concerns\ShowsResources; +use Flarum\Api\Resource\AbstractResource; use Flarum\Database\Eloquent\Collection; use RuntimeException; use Tobyz\JsonApiServer\Resource\Creatable; @@ -56,8 +57,11 @@ class Create extends Endpoint $data = $this->parseData($context); + /** @var AbstractResource $resource */ + $resource = $context->resource($data['type']); + $context = $context - ->withResource($resource = $context->resource($data['type'])) + ->withResource($resource) ->withModel($model = $collection->newModel($context)); $this->assertFieldsValid($context, $data); diff --git a/framework/core/src/Api/Endpoint/Delete.php b/framework/core/src/Api/Endpoint/Delete.php index e2ad756c3..e2c0b5b15 100644 --- a/framework/core/src/Api/Endpoint/Delete.php +++ b/framework/core/src/Api/Endpoint/Delete.php @@ -12,6 +12,7 @@ namespace Flarum\Api\Endpoint; use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; +use Flarum\Api\Resource\AbstractResource; use Nyholm\Psr7\Response; use RuntimeException; use Tobyz\JsonApiServer\Resource\Deletable; @@ -36,9 +37,10 @@ class Delete extends Endpoint ->action(function (Context $context) { $model = $context->model; - $context = $context->withResource( - $resource = $context->resource($context->collection->resource($model, $context)), - ); + /** @var AbstractResource $resource */ + $resource = $context->resource($context->collection->resource($model, $context)); + + $context = $context->withResource($resource); if (! $resource instanceof Deletable) { throw new RuntimeException( diff --git a/framework/core/src/Api/Endpoint/Endpoint.php b/framework/core/src/Api/Endpoint/Endpoint.php index 31e21c9c9..9250f02d6 100644 --- a/framework/core/src/Api/Endpoint/Endpoint.php +++ b/framework/core/src/Api/Endpoint/Endpoint.php @@ -16,6 +16,7 @@ use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Flarum\Api\Endpoint\Concerns\ShowsResources; +use Flarum\Api\Resource\AbstractResource; use Psr\Http\Message\ResponseInterface as Response; use RuntimeException; use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources; @@ -24,7 +25,7 @@ use Tobyz\JsonApiServer\Exception\MethodNotAllowedException; use function Tobyz\JsonApiServer\json_api_response; -class Endpoint implements EndpointInterface +class Endpoint implements \Tobyz\JsonApiServer\Endpoint\Endpoint { use ShowsResources; use FindsResources; @@ -116,8 +117,11 @@ class Endpoint implements EndpointInterface throw new MethodNotAllowedException(); } + /** @var AbstractResource $collection */ + $collection = $context->collection; + $context = $context->withModelId( - $context->collection->id($context) + $collection->id($context) ); if ($context->modelId) { diff --git a/framework/core/src/Api/Endpoint/EndpointInterface.php b/framework/core/src/Api/Endpoint/EndpointInterface.php deleted file mode 100644 index 4b3b8123e..000000000 --- a/framework/core/src/Api/Endpoint/EndpointInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -applySorts($query, $context); $this->applyFilters($query, $context); - $pagination?->apply($query); + if ($pagination && method_exists($pagination, 'apply')) { + $pagination->apply($query); + } } return $context; @@ -131,6 +134,7 @@ class Index extends Endpoint throw new RuntimeException('The Index endpoint query closure must return a Context instance.'); } } else { + /** @var Context $context */ $context = $context->withQuery($query); $this->applySorts($query, $context); @@ -159,6 +163,7 @@ class Index extends Endpoint return compact('models', 'meta', 'pagination', 'total'); }) ->beforeSerialization(function (Context $context, array $results) { + // @phpstan-ignore-next-line $this->loadRelations(Collection::make($results['models']), $context, $this->getInclude($context)); }) ->response(function (Context $context, array $results): Response { @@ -204,7 +209,13 @@ class Index extends Endpoint return; } - $sorts = $context->collection->resolveSorts(); + $collection = $context->collection; + + if (! $collection instanceof AbstractResource) { + throw new RuntimeException('The collection ' . $collection::class . ' must extend ' . AbstractResource::class); + } + + $sorts = $collection->resolveSorts(); foreach (parse_sort_string($sortString) as [$name, $direction]) { foreach ($sorts as $field) { @@ -232,8 +243,16 @@ class Index extends Endpoint ]); } + $collection = $context->collection; + + if (! $collection instanceof \Tobyz\JsonApiServer\Resource\Listable) { + throw new RuntimeException( + sprintf('%s must implement %s', $collection::class, \Tobyz\JsonApiServer\Resource\Listable::class), + ); + } + try { - apply_filters($query, $filters, $context->collection, $context); + apply_filters($query, $filters, $collection, $context); } catch (Sourceable $e) { throw $e->prependSource(['parameter' => 'filter']); } diff --git a/framework/core/src/Api/Endpoint/Update.php b/framework/core/src/Api/Endpoint/Update.php index d78660a75..9b36795e1 100644 --- a/framework/core/src/Api/Endpoint/Update.php +++ b/framework/core/src/Api/Endpoint/Update.php @@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\IncludesData; use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData; use Flarum\Api\Endpoint\Concerns\ShowsResources; +use Flarum\Api\Resource\AbstractResource; use Flarum\Database\Eloquent\Collection; use RuntimeException; use Tobyz\JsonApiServer\Resource\Updatable; @@ -38,9 +39,10 @@ class Update extends Endpoint ->action(function (Context $context): object { $model = $context->model; - $context = $context->withResource( - $resource = $context->resource($context->collection->resource($model, $context)), - ); + /** @var AbstractResource $resource */ + $resource = $context->resource($context->collection->resource($model, $context)); + + $context = $context->withResource($resource); if (! $resource instanceof Updatable) { throw new RuntimeException( diff --git a/framework/core/src/Api/JsonApi.php b/framework/core/src/Api/JsonApi.php index f4c7e1fcc..357adc5da 100644 --- a/framework/core/src/Api/JsonApi.php +++ b/framework/core/src/Api/JsonApi.php @@ -9,14 +9,16 @@ namespace Flarum\Api; -use Flarum\Api\Endpoint\EndpointInterface; +use Flarum\Api\Endpoint\Endpoint; use Flarum\Api\Resource\AbstractDatabaseResource; +use Flarum\Api\Resource\AbstractResource; use Flarum\Http\RequestUtil; use Illuminate\Contracts\Container\Container; use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\Uri; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use RuntimeException; use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\Exception\ResourceNotFoundException; use Tobyz\JsonApiServer\JsonApi as BaseJsonApi; @@ -57,9 +59,13 @@ class JsonApi extends BaseJsonApi ->withEndpoint($this->findEndpoint($collection)); } - protected function findEndpoint(?Collection $collection): EndpointInterface + protected function findEndpoint(?Collection $collection): Endpoint { - /** @var EndpointInterface $endpoint */ + if (! $collection instanceof AbstractResource) { + throw new RuntimeException('Resource ' . $collection::class . ' must extend ' . AbstractResource::class); + } + + /** @var Endpoint $endpoint */ foreach ($collection->resolveEndpoints() as $endpoint) { if ($endpoint->name === $this->endpointName) { return $endpoint; @@ -152,13 +158,19 @@ class JsonApi extends BaseJsonApi $context = $context->withInternal($key, $value); } + $endpoint = $context->endpoint; + + if (! $endpoint instanceof Endpoint) { + throw new RuntimeException('The endpoint ' . $endpoint::class . ' must extend ' . Endpoint::class); + } + $context = $context->withRequest( $request - ->withMethod($context->endpoint->method) - ->withUri(new Uri($context->endpoint->path)) + ->withMethod($endpoint->method) + ->withUri(new Uri($endpoint->path)) ); - return $context->endpoint->process($context); + return $endpoint->process($context); } public function validateQueryParameters(Request $request): void diff --git a/framework/core/src/Api/Resource/AbstractDatabaseResource.php b/framework/core/src/Api/Resource/AbstractDatabaseResource.php index de024b1be..e54e4918b 100644 --- a/framework/core/src/Api/Resource/AbstractDatabaseResource.php +++ b/framework/core/src/Api/Resource/AbstractDatabaseResource.php @@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Str; use InvalidArgumentException; @@ -31,7 +32,7 @@ use Tobyz\JsonApiServer\Schema\Type\DateTime; /** * @template M of Model - * @extends AbstractResource + * @extends AbstractResource */ abstract class AbstractDatabaseResource extends AbstractResource implements Contracts\Findable, @@ -42,10 +43,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements Contracts\Updatable, Contracts\Deletable { - use DispatchEventsTrait { - dispatchEventsFor as traitDispatchEventsFor; - } - abstract public function model(): string; /** @@ -89,7 +86,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements * @param M $model * @param FlarumContext $context */ - protected function getAttributeValue(Model $model, Field $field, Context $context) + protected function getAttributeValue(Model $model, Field $field, Context $context): mixed { if ($field instanceof RelationAggregator && ($aggregate = $field->getRelationAggregate())) { $relationName = $aggregate['relation']; @@ -98,6 +95,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements return $model->getAttribute($this->property($field)); } + /** @var Relationship|null $relationship */ $relationship = collect($context->fields($this))->first(fn ($f) => $f->name === $relationName); if (! $relationship) { @@ -120,7 +118,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements * @param M $model * @param FlarumContext $context */ - protected function getRelationshipValue(Model $model, Relationship $field, Context $context) + protected function getRelationshipValue(Model $model, Relationship $field, Context $context): mixed { $method = $this->method($field); @@ -135,7 +133,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements if ($key = $model->getAttribute($relation->getForeignKeyName())) { if ($relation instanceof MorphTo) { $morphType = $model->{$relation->getMorphType()}; - $morphType = MorphTo::getMorphedModel($morphType) ?? $morphType; $related = $relation->createModelByType($morphType); } else { $related = $relation->getRelated(); @@ -222,10 +219,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements */ public function find(string $id, Context $context): ?object { - if ($id === null) { - return null; - } - return $this->query($context)->find($id); } @@ -282,27 +275,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements } } - /** - * @param M $model - * @param FlarumContext $context - */ - public function createAction(object $model, Context $context): object - { - $model = $this->creating($model, $context) ?: $model; - - $model = $this->saving($model, $context) ?: $model; - - $model = $this->create($model, $context); - - $model = $this->saved($model, $context) ?: $model; - - $model = $this->created($model, $context) ?: $model; - - $this->dispatchEventsFor($model, $context->getActor()); - - return $model; - } - /** * @param M $model * @param FlarumContext $context @@ -314,27 +286,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements return $model; } - /** - * @param M $model - * @param FlarumContext $context - */ - public function updateAction(object $model, Context $context): object - { - $model = $this->updating($model, $context) ?: $model; - - $model = $this->saving($model, $context) ?: $model; - - $this->update($model, $context); - - $model = $this->saved($model, $context) ?: $model; - - $model = $this->updated($model, $context) ?: $model; - - $this->dispatchEventsFor($model, $context->getActor()); - - return $model; - } - /** * @param M $model * @param FlarumContext $context @@ -355,21 +306,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements $model->save(); } - /** - * @param M $model - * @param FlarumContext $context - */ - public function deleteAction(object $model, Context $context): void - { - $this->deleting($model, $context); - - $this->delete($model, $context); - - $this->deleted($model, $context); - - $this->dispatchEventsFor($model, $context->getActor()); - } - /** * @param M $model * @param FlarumContext $context @@ -406,91 +342,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.'); } - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function creating(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function updating(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function saving(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function saved(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function created(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - * @return M|null - */ - public function updated(object $model, Context $context): ?object - { - return $model; - } - - /** - * @param M $model - * @param FlarumContext $context - */ - public function deleting(object $model, Context $context): void - { - // - } - - /** - * @param M $model - * @param FlarumContext $context - */ - public function deleted(object $model, Context $context): void - { - // - } - - public function dispatchEventsFor(mixed $entity, User $actor = null): void - { - if (method_exists($entity, 'releaseEvents')) { - $this->traitDispatchEventsFor($entity, $actor); - } - } - /** * @param FlarumContext $context */ diff --git a/framework/core/src/Api/Resource/AbstractResource.php b/framework/core/src/Api/Resource/AbstractResource.php index b37d441fc..aaf611e06 100644 --- a/framework/core/src/Api/Resource/AbstractResource.php +++ b/framework/core/src/Api/Resource/AbstractResource.php @@ -12,18 +12,19 @@ namespace Flarum\Api\Resource; use Flarum\Api\Context; use Flarum\Api\Resource\Concerns\Bootable; use Flarum\Api\Resource\Concerns\Extendable; +use Flarum\Api\Resource\Concerns\HasHooks; use Flarum\Api\Resource\Concerns\HasSortMap; use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource; /** * @template M of object - * @extends BaseResource */ abstract class AbstractResource extends BaseResource { use Bootable; use Extendable; use HasSortMap; + use HasHooks; public function id(Context $context): ?string { diff --git a/framework/core/src/Api/Resource/Concerns/HasHooks.php b/framework/core/src/Api/Resource/Concerns/HasHooks.php new file mode 100644 index 000000000..aa98cb9d2 --- /dev/null +++ b/framework/core/src/Api/Resource/Concerns/HasHooks.php @@ -0,0 +1,182 @@ +creating($model, $context) ?: $model; + + $model = $this->saving($model, $context) ?: $model; + + $model = $this->create($model, $context); + + $model = $this->saved($model, $context) ?: $model; + + $model = $this->created($model, $context) ?: $model; + + $this->dispatchEventsFor($model, $context->getActor()); + + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + */ + public function updateAction(object $model, Context $context): object + { + if (! $this instanceof Updatable) { + throw new RuntimeException( + sprintf('%s must implement %s', get_class($this), Updatable::class), + ); + } + + $model = $this->updating($model, $context) ?: $model; + + $model = $this->saving($model, $context) ?: $model; + + $this->update($model, $context); + + $model = $this->saved($model, $context) ?: $model; + + $model = $this->updated($model, $context) ?: $model; + + $this->dispatchEventsFor($model, $context->getActor()); + + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + */ + public function deleteAction(object $model, Context $context): void + { + if (! $this instanceof Deletable) { + throw new RuntimeException( + sprintf('%s must implement %s', get_class($this), Deletable::class), + ); + } + + $this->deleting($model, $context); + + $this->delete($model, $context); + + $this->deleted($model, $context); + + $this->dispatchEventsFor($model, $context->getActor()); + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function creating(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function updating(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function saving(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function saved(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function created(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + * @return M|null + */ + public function updated(object $model, Context $context): ?object + { + return $model; + } + + /** + * @param M $model + * @param FlarumContext $context + */ + public function deleting(object $model, Context $context): void + { + // + } + + /** + * @param M $model + * @param FlarumContext $context + */ + public function deleted(object $model, Context $context): void + { + // + } + + public function dispatchEventsFor(mixed $entity, User $actor = null): void + { + if (method_exists($entity, 'releaseEvents')) { + $this->traitDispatchEventsFor($entity, $actor); + } + } +} diff --git a/framework/core/src/Api/Resource/DiscussionResource.php b/framework/core/src/Api/Resource/DiscussionResource.php index 58e7afbbb..cc6ba1580 100644 --- a/framework/core/src/Api/Resource/DiscussionResource.php +++ b/framework/core/src/Api/Resource/DiscussionResource.php @@ -231,10 +231,13 @@ class DiscussionResource extends AbstractDatabaseResource $offset = $endpoint->extractOffsetValue($context, $endpoint->defaultExtracts($context)); } + /** @var Endpoint\Endpoint $endpoint */ + $endpoint = $context->endpoint; + $posts = $discussion->posts() ->whereVisibleTo($actor) - ->with($context->endpoint->getEagerLoadsFor('posts', $context)) - ->with($context->endpoint->getWhereEagerLoadsFor('posts', $context)) + ->with($endpoint->getEagerLoadsFor('posts', $context)) + ->with($endpoint->getWhereEagerLoadsFor('posts', $context)) ->orderBy('number') ->skip($offset) ->take($limit) diff --git a/framework/core/src/Api/Resource/EloquentBuffer.php b/framework/core/src/Api/Resource/EloquentBuffer.php index 6b33a9ca9..09557accc 100644 --- a/framework/core/src/Api/Resource/EloquentBuffer.php +++ b/framework/core/src/Api/Resource/EloquentBuffer.php @@ -9,12 +9,13 @@ namespace Flarum\Api\Resource; +use Flarum\Api\Context; +use Flarum\Api\Endpoint\Endpoint; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\Relation; -use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Laravel\Field\ToMany; use Tobyz\JsonApiServer\Laravel\Field\ToOne; use Tobyz\JsonApiServer\Schema\Field\Relationship; @@ -25,21 +26,21 @@ abstract class EloquentBuffer public static function add(Model $model, string $relationName, ?array $aggregate = null): void { - static::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'][] = $model; + self::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'][] = $model; } public static function getBuffer(Model $model, string $relationName, ?array $aggregate = null): ?array { - return static::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'] ?? null; + return self::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'] ?? null; } public static function setBuffer(Model $model, string $relationName, ?array $aggregate, array $buffer): void { - static::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'] = $buffer; + self::$buffer[get_class($model)][$relationName][$aggregate ? $aggregate['column'].$aggregate['function'] : 'normal'] = $buffer; } /** - * @param array{relation: string, column: string, function: string, constrain: Closure}|null $aggregate + * @param array{relation: string, column: string, function: string, constrain: callable|null}|null $aggregate */ public static function load( Model $model, @@ -48,7 +49,7 @@ abstract class EloquentBuffer Context $context, ?array $aggregate = null, ): void { - if (! ($models = static::getBuffer($model, $relationName, $aggregate))) { + if (! ($models = self::getBuffer($model, $relationName, $aggregate))) { return; } @@ -64,6 +65,7 @@ abstract class EloquentBuffer // may be multiple if this is a polymorphic relationship. We // start by getting the resource types this relationship // could possibly contain. + /** @var AbstractDatabaseResource[] $resources */ $resources = $context->api->resources; if ($type = $relationship->collections) { @@ -81,9 +83,12 @@ abstract class EloquentBuffer if ($resource instanceof AbstractDatabaseResource && ! isset($constrain[$modelClass])) { $constrain[$modelClass] = function (Builder $query) use ($resource, $context, $relationship, $aggregate) { if (! $aggregate) { + /** @var Endpoint $endpoint */ + $endpoint = $context->endpoint; + $query - ->with($context->endpoint->getEagerLoadsFor($relationship->name, $context)) - ->with($context->endpoint->getWhereEagerLoadsFor($relationship->name, $context)); + ->with($endpoint->getEagerLoadsFor($relationship->name, $context)) + ->with($endpoint->getWhereEagerLoadsFor($relationship->name, $context)); } $resource->scope($query, $context); @@ -115,8 +120,10 @@ abstract class EloquentBuffer // Set the inverse relation on the loaded relations. $collection->each(function (Model $model) use ($relationName, $relationship) { - /** @var Model|Collection $related */ - if ($related = $model->getRelation($relationName)) { + /** @var Model|Collection|null $related */ + $related = $model->getRelation($relationName); + + if ($related) { $inverse = $relationship->inverse ?? str($model::class)->afterLast('\\')->camel()->toString(); $related = $related instanceof Collection ? $related : [$related]; @@ -132,6 +139,6 @@ abstract class EloquentBuffer $collection->loadAggregate([$relationName => $loader], $aggregate['column'], $aggregate['function']); } - static::setBuffer($model, $relationName, $aggregate, []); + self::setBuffer($model, $relationName, $aggregate, []); } } diff --git a/framework/core/src/Api/Resource/PostResource.php b/framework/core/src/Api/Resource/PostResource.php index 6fc513450..c02cbd8db 100644 --- a/framework/core/src/Api/Resource/PostResource.php +++ b/framework/core/src/Api/Resource/PostResource.php @@ -58,6 +58,7 @@ class PostResource extends AbstractDatabaseResource $query->whereVisibleTo($context->getActor()); } + /** @inheritDoc */ public function newModel(\Tobyz\JsonApiServer\Context $context): object { if ($context->creating(self::class)) { diff --git a/framework/core/src/Api/Schema/Contracts/RelationAggregator.php b/framework/core/src/Api/Schema/Contracts/RelationAggregator.php index e0dd1661c..8077e5539 100644 --- a/framework/core/src/Api/Schema/Contracts/RelationAggregator.php +++ b/framework/core/src/Api/Schema/Contracts/RelationAggregator.php @@ -9,12 +9,14 @@ namespace Flarum\Api\Schema\Contracts; +use Closure; + interface RelationAggregator { public function relationAggregate(string $relation, string $column, string $function): static; /** - * @return array{relation: string, column: string, function: string}|null + * @return array{relation: string, column: string, function: string, constrain: Closure|null}|null */ public function getRelationAggregate(): ?array; } diff --git a/framework/core/src/Extend/ApiResource.php b/framework/core/src/Extend/ApiResource.php index 68faea8fb..66e6e9f2e 100644 --- a/framework/core/src/Extend/ApiResource.php +++ b/framework/core/src/Extend/ApiResource.php @@ -9,13 +9,12 @@ namespace Flarum\Extend; -use Flarum\Api\Endpoint\EndpointInterface; +use Flarum\Api\Endpoint\Endpoint; use Flarum\Extension\Extension; use Flarum\Foundation\ContainerUtil; use Illuminate\Contracts\Container\Container; use ReflectionClass; use RuntimeException; -use Tobyz\JsonApiServer\Endpoint\Endpoint; use Tobyz\JsonApiServer\Resource\Resource; use Tobyz\JsonApiServer\Schema\Field\Field; use Tobyz\JsonApiServer\Schema\Sort; @@ -179,7 +178,7 @@ class ApiResource implements ExtenderInterface $resourceClass::mutateEndpoints( /** - * @var EndpointInterface[] $endpoints + * @var Endpoint[] $endpoints */ function (array $endpoints, Resource $resource) use ($container): array { foreach ($this->endpoints as $newEndpointsCallback) { @@ -203,8 +202,8 @@ class ApiResource implements ExtenderInterface $mutateEndpoint = ContainerUtil::wrapCallback($mutator, $container); $endpoint = $mutateEndpoint($endpoint, $resource); - if (! $endpoint instanceof EndpointInterface) { - throw new RuntimeException('The endpoint mutator must return an instance of '.EndpointInterface::class); + if (! $endpoint instanceof Endpoint) { + throw new RuntimeException('The endpoint mutator must return an instance of '.Endpoint::class); } } } diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index 92b95be28..f9ee1d08e 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -18,6 +18,7 @@ parameters: # We know for a fact the JsonApi object used internally is always the Flarum one. - stubs/Tobyz/JsonApiServer/JsonApi.stub + - stubs/Tobyz/JsonApiServer/Context.stub services: - diff --git a/php-packages/phpstan/phpstan-baseline.neon b/php-packages/phpstan/phpstan-baseline.neon index 19c3148b3..3f70ccbbb 100644 --- a/php-packages/phpstan/phpstan-baseline.neon +++ b/php-packages/phpstan/phpstan-baseline.neon @@ -40,3 +40,7 @@ parameters: # This assumes that the phpdoc telling it it's not nullable is correct, that's not the case for internal Laravel typings. - message: '#^Property [A-z0-9-_:$,\\]+ \([A-z]+\) on left side of \?\? is not nullable\.$#' + + # Ignore overriden classes from packages so that it's always easier to keep track of what's being overriden. + - message: '#^Method Flarum\\Api\\Serializer\:\:[A-z0-9_]+\(\) has parameter \$[A-z0-9_]+ with no type specified\.$#' + - message: '#^Method Flarum\\Api\\Endpoint\\[A-z0-9_]+\:\:[A-z0-9_]+\(\) has parameter \$[A-z0-9_]+ with no type specified\.$#' diff --git a/php-packages/phpstan/stubs/Tobyz/JsonApiServer/Context.stub b/php-packages/phpstan/stubs/Tobyz/JsonApiServer/Context.stub new file mode 100644 index 000000000..a50fab597 --- /dev/null +++ b/php-packages/phpstan/stubs/Tobyz/JsonApiServer/Context.stub @@ -0,0 +1,11 @@ +