Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using callbacks with load relations extender #3116

Merged
merged 8 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 67 additions & 7 deletions src/Api/Controller/AbstractSerializeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,15 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
protected static $beforeSerializationCallbacks = [];

/**
* @var array
* @var string[]
*/
protected static $loadRelations = [];

/**
* @var array<string, callable>
*/
protected static $loadRelationCallables = [];

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -149,6 +154,8 @@ abstract protected function createElement($data, SerializerInterface $serializer

/**
* Returns the relations to load added by extenders.
*
* @return string[]
*/
protected function getRelationsToLoad(): array
{
Expand All @@ -164,15 +171,34 @@ protected function getRelationsToLoad(): array
}

/**
* Eager loads the required relationships.
* Returns the relation callables to load added by extenders.
*
* @param Collection $models
* @param array $relations
* @return void
* @return array<string, callable>
*/
protected function getRelationCallablesToLoad(): array
{
$addedRelationCallables = [];

foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
if (isset(static::$loadRelationCallables[$class])) {
$addedRelationCallables = array_merge($addedRelationCallables, static::$loadRelationCallables[$class]);
}
}

return $addedRelationCallables;
}

/**
* Eager loads the required relationships.
*/
protected function loadRelations(Collection $models, array $relations): void
protected function loadRelations(Collection $models, array $relations, ServerRequestInterface $request = null): void
{
$addedRelations = $this->getRelationsToLoad();
$addedRelationCallables = $this->getRelationCallablesToLoad();

foreach ($addedRelationCallables as $name => $relation) {
$addedRelations[] = $name;
}

if (! empty($addedRelations)) {
usort($addedRelations, function ($a, $b) {
Expand All @@ -194,7 +220,29 @@ protected function loadRelations(Collection $models, array $relations): void

if (! empty($relations)) {
$relations = array_unique($relations);
$models->loadMissing($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);
}
}

Expand Down Expand Up @@ -430,4 +478,16 @@ public static function setLoadRelations(string $controllerClass, array $relation

static::$loadRelations[$controllerClass] = array_merge(static::$loadRelations[$controllerClass], $relations);
}

/**
* @internal
*/
public static function setLoadRelationCallables(string $controllerClass, array $relations)
{
if (! isset(static::$loadRelationCallables[$controllerClass])) {
static::$loadRelationCallables[$controllerClass] = [];
}

static::$loadRelationCallables[$controllerClass] = array_merge(static::$loadRelationCallables[$controllerClass], $relations);
}
}
2 changes: 1 addition & 1 deletion src/Api/Controller/ListDiscussionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ protected function data(ServerRequestInterface $request, Document $document)

$results = $results->getResults();

$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);

if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {
foreach ($results as $discussion) {
Expand Down
6 changes: 5 additions & 1 deletion src/Api/Controller/ListGroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ protected function data(ServerRequestInterface $request, Document $document)
$queryResults->areMoreResults() ? null : 0
);

return $queryResults->getResults();
$results = $queryResults->getResults();

$this->loadRelations($results, [], $request);

return $results;
}
}
2 changes: 1 addition & 1 deletion src/Api/Controller/ListNotificationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ protected function data(ServerRequestInterface $request, Document $document)

$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset);

$this->loadRelations($notifications, array_diff($include, ['subject.discussion']));
$this->loadRelations($notifications, array_diff($include, ['subject.discussion']), $request);

$notifications = $notifications->all();

Expand Down
2 changes: 1 addition & 1 deletion src/Api/Controller/ListPostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ protected function data(ServerRequestInterface $request, Document $document)

$results = $results->getResults();

$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);

return $results;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Api/Controller/ListUsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ protected function data(ServerRequestInterface $request, Document $document)

$results = $results->getResults();

$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);

return $results;
}
Expand Down
24 changes: 23 additions & 1 deletion src/Extend/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ApiController implements ExtenderInterface
private $removeSortFields = [];
private $sort;
private $load = [];
private $loadCallables = [];

/**
* @param string $controllerClass: The ::class attribute of the controller you are modifying.
Expand Down Expand Up @@ -303,7 +304,27 @@ public function setSort(array $sort, $callback = null): self
*/
public function load($relations): self
{
$this->load = array_merge($this->load, (array) $relations);
$this->load = array_merge($this->load, array_map('strval', (array) $relations));

return $this;
}

/**
* Allows loading a relationship with additional query modification.
*
* @param string $relation: Relationship name, see load method description.
* @param callable(\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Relations\Relation, \Psr\Http\Message\ServerRequestInterface|null, array): void $callback
*
* The callback to modify the query, should accept:
* - \Illuminate\Database\Query\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.
*
* @return self
*/
public function loadWhere(string $relation, callable $callback): self
{
$this->loadCallables = array_merge($this->loadCallables, [$relation => $callback]);

return $this;
}
Expand Down Expand Up @@ -375,6 +396,7 @@ public function extend(Container $container, Extension $extension = null)
}

AbstractSerializeController::setLoadRelations($this->controllerClass, $this->load);
AbstractSerializeController::setLoadRelationCallables($this->controllerClass, $this->loadCallables);
}

/**
Expand Down
120 changes: 120 additions & 0 deletions tests/integration/extenders/ApiControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,126 @@ public function custom_second_level_relation_is_not_loaded_when_first_level_is_n

$this->assertTrue($users->pluck('firstLevelRelation')->filter->relationLoaded('secondLevelRelation')->isEmpty());
}

/**
* @test
*/
public function custom_callable_first_level_relation_is_loaded_if_added()
{
$users = null;

$this->extend(
(new Extend\Model(User::class))
->hasOne('firstLevelRelation', Post::class, 'user_id'),
(new Extend\ApiController(ListUsersController::class))
->loadWhere('firstLevelRelation', function ($query, $request) {})
->prepareDataForSerialization(function ($controller, $data) use (&$users) {
$users = $data;

return [];
})
);

$this->send(
$this->request('GET', '/api/users', [
'authenticatedAs' => 1,
])
);

$this->assertFalse($users->filter->relationLoaded('firstLevelRelation')->isEmpty());
}

/**
* @test
*/
public function custom_callable_second_level_relation_is_loaded_if_added()
{
$users = null;

$this->extend(
(new Extend\Model(User::class))
->hasOne('firstLevelRelation', Post::class, 'user_id'),
(new Extend\Model(Post::class))
->belongsTo('secondLevelRelation', Discussion::class),
(new Extend\ApiController(ListUsersController::class))
->load('firstLevelRelation')
->loadWhere('firstLevelRelation.secondLevelRelation', function ($query, $request) {})
->prepareDataForSerialization(function ($controller, $data) use (&$users) {
$users = $data;

return [];
})
);

$this->send(
$this->request('GET', '/api/users', [
'authenticatedAs' => 1,
])
);

$this->assertFalse($users->pluck('firstLevelRelation')->filter->relationLoaded('secondLevelRelation')->isEmpty());
}

/**
* @test
*/
public function custom_callable_second_level_relation_is_not_loaded_when_first_level_is_not()
{
$users = null;

$this->extend(
(new Extend\Model(User::class))
->hasOne('firstLevelRelation', Post::class, 'user_id'),
(new Extend\Model(Post::class))
->belongsTo('secondLevelRelation', Discussion::class),
(new Extend\ApiController(ListUsersController::class))
->loadWhere('firstLevelRelation.secondLevelRelation', function ($query, $request) {})
->prepareDataForSerialization(function ($controller, $data) use (&$users) {
$users = $data;

return [];
})
);

$this->send(
$this->request('GET', '/api/users', [
'authenticatedAs' => 1,
])
);

$this->assertTrue($users->pluck('firstLevelRelation')->filter->relationLoaded('secondLevelRelation')->isEmpty());
}

/**
* @test
*/
public function custom_callable_second_level_relation_is_loaded_when_first_level_is()
{
$users = null;

$this->extend(
(new Extend\Model(User::class))
->hasOne('firstLevelRelation', Post::class, 'user_id'),
(new Extend\Model(Post::class))
->belongsTo('secondLevelRelation', Discussion::class),
(new Extend\ApiController(ListUsersController::class))
->loadWhere('firstLevelRelation', function ($query, $request) {})
->loadWhere('firstLevelRelation.secondLevelRelation', function ($query, $request) {})
->prepareDataForSerialization(function ($controller, $data) use (&$users) {
$users = $data;

return [];
})
);

$this->send(
$this->request('GET', '/api/users', [
'authenticatedAs' => 1,
])
);

$this->assertTrue($users->pluck('firstLevelRelation')->filter->relationLoaded('secondLevelRelation')->isNotEmpty());
}
}

class CustomDiscussionSerializer extends DiscussionSerializer
Expand Down