1
0
mirror of https://github.com/flarum/core.git synced 2025-08-04 07:27:39 +02:00

chore: adapt

This commit is contained in:
Sami Mazouz
2024-05-10 20:29:44 +01:00
parent d9e5ab4f11
commit aa39d0c11b
25 changed files with 323 additions and 221 deletions

View File

@@ -45,13 +45,13 @@ return [
->fields(PostResourceFields::class) ->fields(PostResourceFields::class)
->endpoint( ->endpoint(
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class], [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']); return $endpoint->addDefaultInclude(['likes']);
} }
), ),
(new Extend\ApiResource(Resource\DiscussionResource::class)) (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']); return $endpoint->addDefaultInclude(['posts.likes']);
}), }),

View File

@@ -63,7 +63,7 @@ return [
(new Extend\ApiResource(Resource\PostResource::class)) (new Extend\ApiResource(Resource\PostResource::class))
->fields(PostResourceFields::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']); return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
}) })
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index { ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
@@ -137,7 +137,7 @@ return [
}), }),
(new Extend\ApiResource(Resource\PostResource::class)) (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']); return $endpoint->eagerLoad(['mentionsTags']);
}), }),
]), ]),

View File

@@ -9,7 +9,7 @@
namespace Flarum\Api; namespace Flarum\Api;
use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Endpoint\Endpoint;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ErrorHandling\JsonApiFormatter; use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
use Flarum\Foundation\ErrorHandling\Registry; use Flarum\Foundation\ErrorHandling\Registry;
@@ -22,7 +22,6 @@ use Flarum\Http\UrlGenerator;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe; use Laminas\Stratigility\MiddlewarePipe;
use ReflectionClass; use ReflectionClass;
use Tobyz\JsonApiServer\Endpoint\Endpoint;
class ApiServiceProvider extends AbstractServiceProvider class ApiServiceProvider extends AbstractServiceProvider
{ {
@@ -199,7 +198,7 @@ class ApiServiceProvider extends AbstractServiceProvider
* None of the injected dependencies should be directly used within * None of the injected dependencies should be directly used within
* the `endpoints` method. Encourage using callbacks. * the `endpoints` method. Encourage using callbacks.
* *
* @var array<EndpointInterface> $endpoints * @var array<Endpoint> $endpoints
*/ */
$endpoints = $resource->resolveEndpoints(true); $endpoints = $resource->resolveEndpoints(true);

View File

@@ -57,6 +57,7 @@ class Context extends BaseContext
$fields = []; $fields = [];
// @phpstan-ignore-next-line
foreach ($resource->resolveFields() as $field) { foreach ($resource->resolveFields() as $field) {
$fields[$field->name] = $field; $fields[$field->name] = $field;
} }
@@ -156,10 +157,13 @@ class Context extends BaseContext
return $new; 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(), '/'); $currentPath = trim($context->path(), '/');
$path = trim($context->collection->name().$this->endpoint->path, '/'); $path = trim($context->collection->name().$endpoint->path, '/');
if (! str_contains($path, '{id}')) { if (! str_contains($path, '{id}')) {
return null; return null;

View File

@@ -10,9 +10,9 @@
namespace Flarum\Api\Endpoint\Concerns; namespace Flarum\Api\Endpoint\Concerns;
use Closure; use Closure;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Resource\AbstractResource;
use Tobyz\JsonApiServer\Schema\Sort; use Tobyz\JsonApiServer\Schema\Sort;
trait ExtractsListingParams trait ExtractsListingParams
@@ -110,11 +110,13 @@ trait ExtractsListingParams
public function getAvailableSorts(Context $context): array public function getAvailableSorts(Context $context): array
{ {
if (! $context->collection instanceof AbstractResource) { $collection = $context->collection;
if (! $collection instanceof AbstractResource) {
return []; return [];
} }
$asc = collect($context->collection->resolveSorts()) $asc = collect($collection->resolveSorts())
->filter(fn (Sort $field) => $field->isVisible($context)) ->filter(fn (Sort $field) => $field->isVisible($context))
->pluck('name') ->pluck('name')
->toArray(); ->toArray();

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Endpoint\Concerns; namespace Flarum\Api\Endpoint\Concerns;
use Closure;
use Flarum\Api\Resource\AbstractDatabaseResource; use Flarum\Api\Resource\AbstractDatabaseResource;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -36,7 +37,7 @@ trait HasEagerLoading
* *
* @param string|string[] $relations * @param string|string[] $relations
*/ */
public function eagerLoad(array|string|callable $relations): static public function eagerLoad(array|string|Closure $relations): static
{ {
if (! is_callable($relations)) { if (! is_callable($relations)) {
$this->loadRelations = array_merge($this->loadRelations, array_map('strval', (array) $relations)); $this->loadRelations = array_merge($this->loadRelations, array_map('strval', (array) $relations));

View File

@@ -9,6 +9,8 @@
namespace Flarum\Api\Endpoint\Concerns; namespace Flarum\Api\Endpoint\Concerns;
use Flarum\Api\Schema\Concerns\FlarumField;
use Flarum\Api\Schema\Concerns\HasValidationRules;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Contracts\Validation\Validator;
use Illuminate\Translation\ArrayLoader; use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator; use Illuminate\Translation\Translator;
@@ -30,6 +32,7 @@ trait SavesAndValidatesData
/** /**
* Assert that the field values within a data object pass validation. * Assert that the field values within a data object pass validation.
* *
* @param \Flarum\Api\Context $context
* @throws UnprocessableEntityException * @throws UnprocessableEntityException
*/ */
protected function assertDataValid(Context $context, array $data): void protected function assertDataValid(Context $context, array $data): void
@@ -49,14 +52,17 @@ trait SavesAndValidatesData
foreach ($context->fields($context->resource) as $field) { foreach ($context->fields($context->resource) as $field) {
$writable = $field->isWritable($context->withField($field)); $writable = $field->isWritable($context->withField($field));
if (! $writable) { if (! $writable || ! in_array(HasValidationRules::class, class_uses_recursive($field))) {
continue; continue;
} }
$type = $field instanceof Attribute ? 'attributes' : 'relationships'; $type = $field instanceof Attribute ? 'attributes' : 'relationships';
// @phpstan-ignore-next-line
$rules[$type] = array_merge($rules[$type], $field->getValidationRules($context)); $rules[$type] = array_merge($rules[$type], $field->getValidationRules($context));
// @phpstan-ignore-next-line
$messages = array_merge($messages, $field->getValidationMessages($context)); $messages = array_merge($messages, $field->getValidationMessages($context));
// @phpstan-ignore-next-line
$attributes = array_merge($attributes, $field->getValidationAttributes($context)); $attributes = array_merge($attributes, $field->getValidationAttributes($context));
} }

View File

@@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\IncludesData; use Flarum\Api\Endpoint\Concerns\IncludesData;
use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData; use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData;
use Flarum\Api\Endpoint\Concerns\ShowsResources; use Flarum\Api\Endpoint\Concerns\ShowsResources;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Database\Eloquent\Collection; use Flarum\Database\Eloquent\Collection;
use RuntimeException; use RuntimeException;
use Tobyz\JsonApiServer\Resource\Creatable; use Tobyz\JsonApiServer\Resource\Creatable;
@@ -56,8 +57,11 @@ class Create extends Endpoint
$data = $this->parseData($context); $data = $this->parseData($context);
/** @var AbstractResource $resource */
$resource = $context->resource($data['type']);
$context = $context $context = $context
->withResource($resource = $context->resource($data['type'])) ->withResource($resource)
->withModel($model = $collection->newModel($context)); ->withModel($model = $collection->newModel($context));
$this->assertFieldsValid($context, $data); $this->assertFieldsValid($context, $data);

View File

@@ -12,6 +12,7 @@ namespace Flarum\Api\Endpoint;
use Flarum\Api\Context; use Flarum\Api\Context;
use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Resource\AbstractResource;
use Nyholm\Psr7\Response; use Nyholm\Psr7\Response;
use RuntimeException; use RuntimeException;
use Tobyz\JsonApiServer\Resource\Deletable; use Tobyz\JsonApiServer\Resource\Deletable;
@@ -36,9 +37,10 @@ class Delete extends Endpoint
->action(function (Context $context) { ->action(function (Context $context) {
$model = $context->model; $model = $context->model;
$context = $context->withResource( /** @var AbstractResource $resource */
$resource = $context->resource($context->collection->resource($model, $context)), $resource = $context->resource($context->collection->resource($model, $context));
);
$context = $context->withResource($resource);
if (! $resource instanceof Deletable) { if (! $resource instanceof Deletable) {
throw new RuntimeException( throw new RuntimeException(

View File

@@ -16,6 +16,7 @@ use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Flarum\Api\Endpoint\Concerns\ShowsResources; use Flarum\Api\Endpoint\Concerns\ShowsResources;
use Flarum\Api\Resource\AbstractResource;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use RuntimeException; use RuntimeException;
use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources; use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources;
@@ -24,7 +25,7 @@ use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
use function Tobyz\JsonApiServer\json_api_response; use function Tobyz\JsonApiServer\json_api_response;
class Endpoint implements EndpointInterface class Endpoint implements \Tobyz\JsonApiServer\Endpoint\Endpoint
{ {
use ShowsResources; use ShowsResources;
use FindsResources; use FindsResources;
@@ -116,8 +117,11 @@ class Endpoint implements EndpointInterface
throw new MethodNotAllowedException(); throw new MethodNotAllowedException();
} }
/** @var AbstractResource $collection */
$collection = $context->collection;
$context = $context->withModelId( $context = $context->withModelId(
$context->collection->id($context) $collection->id($context)
); );
if ($context->modelId) { if ($context->modelId) {

View File

@@ -1,15 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Endpoint;
interface EndpointInterface extends \Tobyz\JsonApiServer\Endpoint\Endpoint
{
//
}

View File

@@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasAuthorization;
use Flarum\Api\Endpoint\Concerns\HasCustomHooks; use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\IncludesData; use Flarum\Api\Endpoint\Concerns\IncludesData;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Api\Resource\Contracts\Countable; use Flarum\Api\Resource\Contracts\Countable;
use Flarum\Api\Resource\Contracts\Listable; use Flarum\Api\Resource\Contracts\Listable;
use Flarum\Api\Serializer; use Flarum\Api\Serializer;
@@ -100,7 +101,9 @@ class Index extends Endpoint
$this->applySorts($query, $context); $this->applySorts($query, $context);
$this->applyFilters($query, $context); $this->applyFilters($query, $context);
$pagination?->apply($query); if ($pagination && method_exists($pagination, 'apply')) {
$pagination->apply($query);
}
} }
return $context; return $context;
@@ -131,6 +134,7 @@ class Index extends Endpoint
throw new RuntimeException('The Index endpoint query closure must return a Context instance.'); throw new RuntimeException('The Index endpoint query closure must return a Context instance.');
} }
} else { } else {
/** @var Context $context */
$context = $context->withQuery($query); $context = $context->withQuery($query);
$this->applySorts($query, $context); $this->applySorts($query, $context);
@@ -159,6 +163,7 @@ class Index extends Endpoint
return compact('models', 'meta', 'pagination', 'total'); return compact('models', 'meta', 'pagination', 'total');
}) })
->beforeSerialization(function (Context $context, array $results) { ->beforeSerialization(function (Context $context, array $results) {
// @phpstan-ignore-next-line
$this->loadRelations(Collection::make($results['models']), $context, $this->getInclude($context)); $this->loadRelations(Collection::make($results['models']), $context, $this->getInclude($context));
}) })
->response(function (Context $context, array $results): Response { ->response(function (Context $context, array $results): Response {
@@ -204,7 +209,13 @@ class Index extends Endpoint
return; 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 (parse_sort_string($sortString) as [$name, $direction]) {
foreach ($sorts as $field) { 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 { try {
apply_filters($query, $filters, $context->collection, $context); apply_filters($query, $filters, $collection, $context);
} catch (Sourceable $e) { } catch (Sourceable $e) {
throw $e->prependSource(['parameter' => 'filter']); throw $e->prependSource(['parameter' => 'filter']);
} }

View File

@@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
use Flarum\Api\Endpoint\Concerns\IncludesData; use Flarum\Api\Endpoint\Concerns\IncludesData;
use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData; use Flarum\Api\Endpoint\Concerns\SavesAndValidatesData;
use Flarum\Api\Endpoint\Concerns\ShowsResources; use Flarum\Api\Endpoint\Concerns\ShowsResources;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Database\Eloquent\Collection; use Flarum\Database\Eloquent\Collection;
use RuntimeException; use RuntimeException;
use Tobyz\JsonApiServer\Resource\Updatable; use Tobyz\JsonApiServer\Resource\Updatable;
@@ -38,9 +39,10 @@ class Update extends Endpoint
->action(function (Context $context): object { ->action(function (Context $context): object {
$model = $context->model; $model = $context->model;
$context = $context->withResource( /** @var AbstractResource $resource */
$resource = $context->resource($context->collection->resource($model, $context)), $resource = $context->resource($context->collection->resource($model, $context));
);
$context = $context->withResource($resource);
if (! $resource instanceof Updatable) { if (! $resource instanceof Updatable) {
throw new RuntimeException( throw new RuntimeException(

View File

@@ -9,14 +9,16 @@
namespace Flarum\Api; namespace Flarum\Api;
use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Endpoint\Endpoint;
use Flarum\Api\Resource\AbstractDatabaseResource; use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Http\RequestUtil; use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri; use Laminas\Diactoros\Uri;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use RuntimeException;
use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException; use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
use Tobyz\JsonApiServer\JsonApi as BaseJsonApi; use Tobyz\JsonApiServer\JsonApi as BaseJsonApi;
@@ -57,9 +59,13 @@ class JsonApi extends BaseJsonApi
->withEndpoint($this->findEndpoint($collection)); ->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) { foreach ($collection->resolveEndpoints() as $endpoint) {
if ($endpoint->name === $this->endpointName) { if ($endpoint->name === $this->endpointName) {
return $endpoint; return $endpoint;
@@ -152,13 +158,19 @@ class JsonApi extends BaseJsonApi
$context = $context->withInternal($key, $value); $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( $context = $context->withRequest(
$request $request
->withMethod($context->endpoint->method) ->withMethod($endpoint->method)
->withUri(new Uri($context->endpoint->path)) ->withUri(new Uri($endpoint->path))
); );
return $context->endpoint->process($context); return $endpoint->process($context);
} }
public function validateQueryParameters(Request $request): void public function validateQueryParameters(Request $request): void

View File

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use InvalidArgumentException; use InvalidArgumentException;
@@ -31,7 +32,7 @@ use Tobyz\JsonApiServer\Schema\Type\DateTime;
/** /**
* @template M of Model * @template M of Model
* @extends AbstractResource<M, FlarumContext> * @extends AbstractResource<M>
*/ */
abstract class AbstractDatabaseResource extends AbstractResource implements abstract class AbstractDatabaseResource extends AbstractResource implements
Contracts\Findable, Contracts\Findable,
@@ -42,10 +43,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
Contracts\Updatable, Contracts\Updatable,
Contracts\Deletable Contracts\Deletable
{ {
use DispatchEventsTrait {
dispatchEventsFor as traitDispatchEventsFor;
}
abstract public function model(): string; abstract public function model(): string;
/** /**
@@ -89,7 +86,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
* @param M $model * @param M $model
* @param FlarumContext $context * @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())) { if ($field instanceof RelationAggregator && ($aggregate = $field->getRelationAggregate())) {
$relationName = $aggregate['relation']; $relationName = $aggregate['relation'];
@@ -98,6 +95,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
return $model->getAttribute($this->property($field)); return $model->getAttribute($this->property($field));
} }
/** @var Relationship|null $relationship */
$relationship = collect($context->fields($this))->first(fn ($f) => $f->name === $relationName); $relationship = collect($context->fields($this))->first(fn ($f) => $f->name === $relationName);
if (! $relationship) { if (! $relationship) {
@@ -120,7 +118,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
* @param M $model * @param M $model
* @param FlarumContext $context * @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); $method = $this->method($field);
@@ -135,7 +133,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
if ($key = $model->getAttribute($relation->getForeignKeyName())) { if ($key = $model->getAttribute($relation->getForeignKeyName())) {
if ($relation instanceof MorphTo) { if ($relation instanceof MorphTo) {
$morphType = $model->{$relation->getMorphType()}; $morphType = $model->{$relation->getMorphType()};
$morphType = MorphTo::getMorphedModel($morphType) ?? $morphType;
$related = $relation->createModelByType($morphType); $related = $relation->createModelByType($morphType);
} else { } else {
$related = $relation->getRelated(); $related = $relation->getRelated();
@@ -222,10 +219,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
*/ */
public function find(string $id, Context $context): ?object public function find(string $id, Context $context): ?object
{ {
if ($id === null) {
return null;
}
return $this->query($context)->find($id); 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 M $model
* @param FlarumContext $context * @param FlarumContext $context
@@ -314,27 +286,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
return $model; 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 M $model
* @param FlarumContext $context * @param FlarumContext $context
@@ -355,21 +306,6 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
$model->save(); $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 M $model
* @param FlarumContext $context * @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.'); 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 * @param FlarumContext $context
*/ */

View File

@@ -12,18 +12,19 @@ namespace Flarum\Api\Resource;
use Flarum\Api\Context; use Flarum\Api\Context;
use Flarum\Api\Resource\Concerns\Bootable; use Flarum\Api\Resource\Concerns\Bootable;
use Flarum\Api\Resource\Concerns\Extendable; use Flarum\Api\Resource\Concerns\Extendable;
use Flarum\Api\Resource\Concerns\HasHooks;
use Flarum\Api\Resource\Concerns\HasSortMap; use Flarum\Api\Resource\Concerns\HasSortMap;
use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource; use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource;
/** /**
* @template M of object * @template M of object
* @extends BaseResource<M, Context>
*/ */
abstract class AbstractResource extends BaseResource abstract class AbstractResource extends BaseResource
{ {
use Bootable; use Bootable;
use Extendable; use Extendable;
use HasSortMap; use HasSortMap;
use HasHooks;
public function id(Context $context): ?string public function id(Context $context): ?string
{ {

View File

@@ -0,0 +1,182 @@
<?php
namespace Flarum\Api\Resource\Concerns;
use Flarum\Api\Context as FlarumContext;
use Flarum\Api\Resource\Contracts\Creatable;
use Flarum\Api\Resource\Contracts\Deletable;
use Flarum\Api\Resource\Contracts\Updatable;
use Flarum\Foundation\DispatchEventsTrait;
use Flarum\User\User;
use RuntimeException;
use Tobyz\JsonApiServer\Context;
/**
* @template M of object
*/
trait HasHooks
{
use DispatchEventsTrait {
dispatchEventsFor as traitDispatchEventsFor;
}
/**
* @param M $model
* @param FlarumContext $context
*/
public function createAction(object $model, Context $context): object
{
if (! $this instanceof Creatable) {
throw new RuntimeException(
sprintf('%s must implement %s', get_class($this), Creatable::class),
);
}
$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
*/
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);
}
}
}

View File

@@ -231,10 +231,13 @@ class DiscussionResource extends AbstractDatabaseResource
$offset = $endpoint->extractOffsetValue($context, $endpoint->defaultExtracts($context)); $offset = $endpoint->extractOffsetValue($context, $endpoint->defaultExtracts($context));
} }
/** @var Endpoint\Endpoint $endpoint */
$endpoint = $context->endpoint;
$posts = $discussion->posts() $posts = $discussion->posts()
->whereVisibleTo($actor) ->whereVisibleTo($actor)
->with($context->endpoint->getEagerLoadsFor('posts', $context)) ->with($endpoint->getEagerLoadsFor('posts', $context))
->with($context->endpoint->getWhereEagerLoadsFor('posts', $context)) ->with($endpoint->getWhereEagerLoadsFor('posts', $context))
->orderBy('number') ->orderBy('number')
->skip($offset) ->skip($offset)
->take($limit) ->take($limit)

View File

@@ -9,12 +9,13 @@
namespace Flarum\Api\Resource; namespace Flarum\Api\Resource;
use Flarum\Api\Context;
use Flarum\Api\Endpoint\Endpoint;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Laravel\Field\ToMany; use Tobyz\JsonApiServer\Laravel\Field\ToMany;
use Tobyz\JsonApiServer\Laravel\Field\ToOne; use Tobyz\JsonApiServer\Laravel\Field\ToOne;
use Tobyz\JsonApiServer\Schema\Field\Relationship; 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 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 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 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( public static function load(
Model $model, Model $model,
@@ -48,7 +49,7 @@ abstract class EloquentBuffer
Context $context, Context $context,
?array $aggregate = null, ?array $aggregate = null,
): void { ): void {
if (! ($models = static::getBuffer($model, $relationName, $aggregate))) { if (! ($models = self::getBuffer($model, $relationName, $aggregate))) {
return; return;
} }
@@ -64,6 +65,7 @@ abstract class EloquentBuffer
// may be multiple if this is a polymorphic relationship. We // may be multiple if this is a polymorphic relationship. We
// start by getting the resource types this relationship // start by getting the resource types this relationship
// could possibly contain. // could possibly contain.
/** @var AbstractDatabaseResource[] $resources */
$resources = $context->api->resources; $resources = $context->api->resources;
if ($type = $relationship->collections) { if ($type = $relationship->collections) {
@@ -81,9 +83,12 @@ abstract class EloquentBuffer
if ($resource instanceof AbstractDatabaseResource && ! isset($constrain[$modelClass])) { if ($resource instanceof AbstractDatabaseResource && ! isset($constrain[$modelClass])) {
$constrain[$modelClass] = function (Builder $query) use ($resource, $context, $relationship, $aggregate) { $constrain[$modelClass] = function (Builder $query) use ($resource, $context, $relationship, $aggregate) {
if (! $aggregate) { if (! $aggregate) {
/** @var Endpoint $endpoint */
$endpoint = $context->endpoint;
$query $query
->with($context->endpoint->getEagerLoadsFor($relationship->name, $context)) ->with($endpoint->getEagerLoadsFor($relationship->name, $context))
->with($context->endpoint->getWhereEagerLoadsFor($relationship->name, $context)); ->with($endpoint->getWhereEagerLoadsFor($relationship->name, $context));
} }
$resource->scope($query, $context); $resource->scope($query, $context);
@@ -115,8 +120,10 @@ abstract class EloquentBuffer
// Set the inverse relation on the loaded relations. // Set the inverse relation on the loaded relations.
$collection->each(function (Model $model) use ($relationName, $relationship) { $collection->each(function (Model $model) use ($relationName, $relationship) {
/** @var Model|Collection $related */ /** @var Model|Collection|null $related */
if ($related = $model->getRelation($relationName)) { $related = $model->getRelation($relationName);
if ($related) {
$inverse = $relationship->inverse ?? str($model::class)->afterLast('\\')->camel()->toString(); $inverse = $relationship->inverse ?? str($model::class)->afterLast('\\')->camel()->toString();
$related = $related instanceof Collection ? $related : [$related]; $related = $related instanceof Collection ? $related : [$related];
@@ -132,6 +139,6 @@ abstract class EloquentBuffer
$collection->loadAggregate([$relationName => $loader], $aggregate['column'], $aggregate['function']); $collection->loadAggregate([$relationName => $loader], $aggregate['column'], $aggregate['function']);
} }
static::setBuffer($model, $relationName, $aggregate, []); self::setBuffer($model, $relationName, $aggregate, []);
} }
} }

View File

@@ -58,6 +58,7 @@ class PostResource extends AbstractDatabaseResource
$query->whereVisibleTo($context->getActor()); $query->whereVisibleTo($context->getActor());
} }
/** @inheritDoc */
public function newModel(\Tobyz\JsonApiServer\Context $context): object public function newModel(\Tobyz\JsonApiServer\Context $context): object
{ {
if ($context->creating(self::class)) { if ($context->creating(self::class)) {

View File

@@ -9,12 +9,14 @@
namespace Flarum\Api\Schema\Contracts; namespace Flarum\Api\Schema\Contracts;
use Closure;
interface RelationAggregator interface RelationAggregator
{ {
public function relationAggregate(string $relation, string $column, string $function): static; 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; public function getRelationAggregate(): ?array;
} }

View File

@@ -9,13 +9,12 @@
namespace Flarum\Extend; namespace Flarum\Extend;
use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Endpoint\Endpoint;
use Flarum\Extension\Extension; use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil; use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use ReflectionClass; use ReflectionClass;
use RuntimeException; use RuntimeException;
use Tobyz\JsonApiServer\Endpoint\Endpoint;
use Tobyz\JsonApiServer\Resource\Resource; use Tobyz\JsonApiServer\Resource\Resource;
use Tobyz\JsonApiServer\Schema\Field\Field; use Tobyz\JsonApiServer\Schema\Field\Field;
use Tobyz\JsonApiServer\Schema\Sort; use Tobyz\JsonApiServer\Schema\Sort;
@@ -179,7 +178,7 @@ class ApiResource implements ExtenderInterface
$resourceClass::mutateEndpoints( $resourceClass::mutateEndpoints(
/** /**
* @var EndpointInterface[] $endpoints * @var Endpoint[] $endpoints
*/ */
function (array $endpoints, Resource $resource) use ($container): array { function (array $endpoints, Resource $resource) use ($container): array {
foreach ($this->endpoints as $newEndpointsCallback) { foreach ($this->endpoints as $newEndpointsCallback) {
@@ -203,8 +202,8 @@ class ApiResource implements ExtenderInterface
$mutateEndpoint = ContainerUtil::wrapCallback($mutator, $container); $mutateEndpoint = ContainerUtil::wrapCallback($mutator, $container);
$endpoint = $mutateEndpoint($endpoint, $resource); $endpoint = $mutateEndpoint($endpoint, $resource);
if (! $endpoint instanceof EndpointInterface) { if (! $endpoint instanceof Endpoint) {
throw new RuntimeException('The endpoint mutator must return an instance of '.EndpointInterface::class); throw new RuntimeException('The endpoint mutator must return an instance of '.Endpoint::class);
} }
} }
} }

View File

@@ -18,6 +18,7 @@ parameters:
# We know for a fact the JsonApi object used internally is always the Flarum one. # We know for a fact the JsonApi object used internally is always the Flarum one.
- stubs/Tobyz/JsonApiServer/JsonApi.stub - stubs/Tobyz/JsonApiServer/JsonApi.stub
- stubs/Tobyz/JsonApiServer/Context.stub
services: services:
- -

View File

@@ -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. # 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\.$#' - 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\.$#'

View File

@@ -0,0 +1,11 @@
<?php
namespace Tobyz\JsonApiServer;
/**
* @mixin \Flarum\Api\Context
*/
class Context
{
}