diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 38ef58492..4ef314e48 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -72,18 +72,24 @@ return [ (new Extend\ApiResource(Resource\DiscussionResource::class)) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index { - return $endpoint->eagerLoad([ - 'firstPost.mentionsUsers', 'firstPost.mentionsPosts', - 'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups', - 'lastPost.mentionsUsers', 'lastPost.mentionsPosts', - 'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups', + return $endpoint->eagerLoadWhenIncluded([ + 'firstPost' => [ + 'firstPost.mentionsUsers', 'firstPost.mentionsPosts', + 'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups', + ], + 'lastPost' => [ + 'lastPost.mentionsUsers', 'lastPost.mentionsPosts', + 'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups', + ], ]); }) ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show { return $endpoint->addDefaultInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion']) - ->eagerLoad([ - 'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user', - 'posts.mentionsPosts.discussion', 'posts.mentionsGroups' + ->eagerLoadWhenIncluded([ + 'posts' => [ + 'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user', + 'posts.mentionsPosts.discussion', 'posts.mentionsGroups' + ], ]); }), @@ -124,10 +130,10 @@ return [ (new Extend\ApiResource(Resource\DiscussionResource::class)) ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show { - return $endpoint->eagerLoad(['posts.mentionsTags']); + return $endpoint->eagerLoadWhenIncluded(['posts' => ['posts.mentionsTags']]); }) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index { - return $endpoint->eagerLoad(['firstPost.mentionsTags', 'lastPost.mentionsTags']); + return $endpoint->eagerLoadWhenIncluded(['firstPost' => ['firstPost.mentionsTags'], 'lastPost' => ['lastPost.mentionsTags']]); }), (new Extend\ApiResource(Resource\PostResource::class)) diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index 363a66f1d..d1dfcd5b6 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -36,10 +36,6 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\Relation; use Psr\Http\Message\ServerRequestInterface; -$eagerLoadTagState = function ($query, ServerRequestInterface $request, array $relations) { - $query->withStateFor(RequestUtil::getActor($request)); -}; - return [ (new Extend\Frontend('forum')) ->js(__DIR__.'/js/dist/forum.js') @@ -95,29 +91,29 @@ return [ return $endpoint->addDefaultInclude(['tags', 'tags.parent']); }), - (new Extend\ApiResource(Resource\DiscussionResource::class)) - ->fields(Api\DiscussionResourceFields::class), - (new Extend\ApiResource(Resource\PostResource::class)) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) { - return $endpoint->eagerLoad('discussion.tags'); + return $endpoint->eagerLoadWhenIncluded(['discussion' => ['discussion.tags']]); }), (new Extend\Conditional()) ->whenExtensionEnabled('flarum-flags', fn () => [ (new Extend\ApiResource(FlagResource::class)) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) { - return $endpoint->eagerLoad(['post.discussion.tags']); + return $endpoint->eagerLoadWhenIncluded(['post.discussion' => ['post.discussion.tags']]); }), ]), (new Extend\ApiResource(Resource\DiscussionResource::class)) + ->fields(Api\DiscussionResourceFields::class) ->endpoint( [Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class], - function (Endpoint\Index|Endpoint\Show|Endpoint\Create $endpoint) use ($eagerLoadTagState) { + function (Endpoint\Index|Endpoint\Show|Endpoint\Create $endpoint) { return $endpoint ->addDefaultInclude(['tags', 'tags.parent']) - ->eagerLoadWhere('tags', $eagerLoadTagState); + ->eagerLoadWhere('tags', function ($query, Context $context) { + $query->withStateFor($context->getActor()); + }); } ), @@ -181,18 +177,12 @@ return [ ]) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) { return $endpoint - ->addDefaultInclude(['eventPostMentionsTags']) - ->eagerLoadWhere('eventPostMentionsTags', function (Relation|Builder $query, ServerRequestInterface $request) { - $query->whereVisibleTo(RequestUtil::getActor($request)); - }); + ->addDefaultInclude(['eventPostMentionsTags']); }), (new Extend\ApiResource(Resource\DiscussionResource::class)) ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) { return $endpoint - ->addDefaultInclude(['posts.eventPostMentionsTags']) - ->eagerLoadWhere('posts.eventPostMentionsTags', function (Relation|Builder $query, ServerRequestInterface $request) { - $query->whereVisibleTo(RequestUtil::getActor($request)); - }); + ->addDefaultInclude(['posts.eventPostMentionsTags']); }), ]; diff --git a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php b/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php deleted file mode 100644 index 0135f1eff..000000000 --- a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php +++ /dev/null @@ -1,155 +0,0 @@ - - */ - protected array $loadRelationCallables = []; - - /** - * Eager loads relationships needed for serializer logic. - * - * First level relationships will be loaded regardless of whether they are included in the response. - * Sub-level relationships will only be loaded if the upper level was included or manually loaded. - * - * @example If a relationship such as: 'relation.subRelation' is specified, - * it will only be loaded if 'relation' is or has been loaded. - * To force load the relationship, both levels have to be specified, - * example: ['relation', 'relation.subRelation']. - * - * @param string|string[] $relations - */ - public function eagerLoad(array|string $relations): self - { - $this->loadRelations = array_merge($this->loadRelations, array_map('strval', (array) $relations)); - - return $this; - } - - /** - * Allows loading a relationship with additional query modification. - * - * @param string $relation: Relationship name, see load method description. - * @template R of Relation - * @param (callable(Builder|R, \Psr\Http\Message\ServerRequestInterface|null, array): void) $callback - * - * The callback to modify the query, should accept: - * - \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query: A query object. - * - \Psr\Http\Message\ServerRequestInterface|null $request: An instance of the request. - * - array $relations: An array of relations that are to be loaded. - */ - public function eagerLoadWhere(string $relation, callable $callback): self - { - $this->loadRelationCallables = array_merge($this->loadRelationCallables, [$relation => $callback]); - - return $this; - } - - /** - * Eager loads the required relationships. - */ - protected function loadRelations(Collection $models, Context $context, array $included = []): void - { - if (! $context->collection instanceof EloquentResource) { - return; - } - - $request = $context->request; - - $included = $this->stringInclude($included); - $models = $models->filter(fn ($model) => $model instanceof Model); - - $addedRelations = $this->loadRelations; - $addedRelationCallables = $this->loadRelationCallables; - - $relations = $included; - - foreach ($addedRelationCallables as $name => $relation) { - $addedRelations[] = $name; - } - - if (! empty($addedRelations)) { - usort($addedRelations, function ($a, $b) { - return substr_count($a, '.') - substr_count($b, '.'); - }); - - foreach ($addedRelations as $relation) { - if (str_contains($relation, '.')) { - $parentRelation = Str::beforeLast($relation, '.'); - - if (! in_array($parentRelation, $relations, true)) { - continue; - } - } - - $relations[] = $relation; - } - } - - if (! empty($relations)) { - $relations = array_unique($relations); - } - - $callableRelations = []; - $nonCallableRelations = []; - - foreach ($relations as $relation) { - if (isset($addedRelationCallables[$relation])) { - $load = $addedRelationCallables[$relation]; - - $callableRelations[$relation] = function ($query) use ($load, $request, $relations) { - $load($query, $request, $relations); - }; - } else { - $nonCallableRelations[] = $relation; - } - } - - if (! empty($callableRelations)) { - $models->loadMissing($callableRelations); - } - - if (! empty($nonCallableRelations)) { - $models->loadMissing($nonCallableRelations); - } - } - - /** - * From format of: 'relation' => [ ...nested ] to ['relation', 'relation.nested'] - */ - private function stringInclude(array $include): array - { - $relations = []; - - foreach ($include as $relation => $nested) { - $relations[] = $relation; - - if (is_array($nested)) { - foreach ($this->stringInclude($nested) as $nestedRelation) { - $relations[] = $relation.'.'.$nestedRelation; - } - } - } - - return $relations; - } -} diff --git a/framework/core/src/Api/Endpoint/Create.php b/framework/core/src/Api/Endpoint/Create.php index a12ee4abf..88c5ec1aa 100644 --- a/framework/core/src/Api/Endpoint/Create.php +++ b/framework/core/src/Api/Endpoint/Create.php @@ -2,27 +2,17 @@ namespace Flarum\Api\Endpoint; -use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasEagerLoading; -use Illuminate\Database\Eloquent\Collection; use Tobyz\JsonApiServer\Endpoint\Create as BaseCreate; class Create extends BaseCreate implements EndpointInterface { use HasAuthorization; - use HasEagerLoading; use HasCustomHooks; public function setUp(): void { parent::setUp(); - - $this->beforeSerialization(function (Context $context, object $model) { - $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - - return $model; - }); } } diff --git a/framework/core/src/Api/Endpoint/Endpoint.php b/framework/core/src/Api/Endpoint/Endpoint.php index a302d5394..4a76de77e 100644 --- a/framework/core/src/Api/Endpoint/Endpoint.php +++ b/framework/core/src/Api/Endpoint/Endpoint.php @@ -5,13 +5,11 @@ namespace Flarum\Api\Endpoint; use Flarum\Api\Endpoint\Concerns\ExtractsListingParams; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Tobyz\JsonApiServer\Endpoint\Endpoint as BaseEndpoint; class Endpoint extends BaseEndpoint implements EndpointInterface { use HasAuthorization; use HasCustomHooks; - use HasEagerLoading; use ExtractsListingParams; } diff --git a/framework/core/src/Api/Endpoint/Index.php b/framework/core/src/Api/Endpoint/Index.php index 659dfaf39..ccb74b7f1 100644 --- a/framework/core/src/Api/Endpoint/Index.php +++ b/framework/core/src/Api/Endpoint/Index.php @@ -6,11 +6,9 @@ use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\ExtractsListingParams; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Flarum\Search\SearchCriteria; use Flarum\Search\SearchManager; use Illuminate\Contracts\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; use Tobyz\JsonApiServer\Endpoint\Index as BaseIndex; use Tobyz\JsonApiServer\Pagination\OffsetPagination; use Tobyz\JsonApiServer\Pagination\Pagination; @@ -18,7 +16,6 @@ use Tobyz\JsonApiServer\Pagination\Pagination; class Index extends BaseIndex implements EndpointInterface { use HasAuthorization; - use HasEagerLoading; use ExtractsListingParams; use HasCustomHooks; @@ -63,9 +60,6 @@ class Index extends BaseIndex implements EndpointInterface } return $context; - }) - ->beforeSerialization(function (Context $context, iterable $models) { - $this->loadRelations(Collection::make($models), $context, $this->getInclude($context)); }); } diff --git a/framework/core/src/Api/Endpoint/Show.php b/framework/core/src/Api/Endpoint/Show.php index 518e22ae8..2f3885137 100644 --- a/framework/core/src/Api/Endpoint/Show.php +++ b/framework/core/src/Api/Endpoint/Show.php @@ -2,27 +2,19 @@ namespace Flarum\Api\Endpoint; -use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\ExtractsListingParams; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasEagerLoading; -use Illuminate\Database\Eloquent\Collection; use Tobyz\JsonApiServer\Endpoint\Show as BaseShow; class Show extends BaseShow implements EndpointInterface { use HasAuthorization; - use HasEagerLoading; use ExtractsListingParams; use HasCustomHooks; public function setUp(): void { - parent::setUp(); - - $this->beforeSerialization(function (Context $context, object $model) { - $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - }); + parent::setUp();; } } diff --git a/framework/core/src/Api/Endpoint/Update.php b/framework/core/src/Api/Endpoint/Update.php index 7c44f075a..0957e06c1 100644 --- a/framework/core/src/Api/Endpoint/Update.php +++ b/framework/core/src/Api/Endpoint/Update.php @@ -2,25 +2,17 @@ namespace Flarum\Api\Endpoint; -use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasEagerLoading; -use Illuminate\Database\Eloquent\Collection; use Tobyz\JsonApiServer\Endpoint\Update as BaseUpdate; class Update extends BaseUpdate implements EndpointInterface { use HasAuthorization; - use HasEagerLoading; use HasCustomHooks; public function setUp(): void { parent::setUp(); - - $this->beforeSerialization(function (Context $context, object $model) { - $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - }); } } diff --git a/framework/core/src/Api/Resource/DiscussionResource.php b/framework/core/src/Api/Resource/DiscussionResource.php index 1dd8c91e6..210238d66 100644 --- a/framework/core/src/Api/Resource/DiscussionResource.php +++ b/framework/core/src/Api/Resource/DiscussionResource.php @@ -94,6 +94,7 @@ class DiscussionResource extends AbstractDatabaseResource 'mostRelevantPost.user' ]) ->defaultSort('-lastPostedAt') + ->eagerLoad('state') ->paginate(), ]; } @@ -184,12 +185,14 @@ class DiscussionResource extends AbstractDatabaseResource ->includable(), Schema\Relationship\ToOne::make('firstPost') ->includable() + ->inverse('discussion') ->type('posts'), Schema\Relationship\ToOne::make('lastPostedUser') ->includable() ->type('users'), Schema\Relationship\ToOne::make('lastPost') ->includable() + ->inverse('discussion') ->type('posts'), Schema\Relationship\ToMany::make('posts') ->withLinkage(function (Context $context) { @@ -216,6 +219,8 @@ class DiscussionResource extends AbstractDatabaseResource $posts = $discussion->posts() ->whereVisibleTo($actor) + ->with($context->endpoint->getEagerLoadsFor('posts', $context)) + ->with($context->endpoint->getWhereEagerLoadsFor('posts', $context)) ->orderBy('number') ->skip($offset) ->take($limit) @@ -236,6 +241,7 @@ class DiscussionResource extends AbstractDatabaseResource Schema\Relationship\ToOne::make('mostRelevantPost') ->visible(fn (Discussion $model, Context $context) => $context->listing()) ->includable() + ->inverse('discussion') ->type('posts'), Schema\Relationship\ToOne::make('hideUser') ->type('users'),