feat: Create loadWhere relations extender (#3116)

This commit is contained in:
Sami Mazouz 2021-11-01 10:45:02 +01:00 committed by GitHub
parent 7b80d3932d
commit 2b163025d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 219 additions and 13 deletions

View File

@ -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}
*/
@ -149,6 +154,8 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
/**
* Returns the relations to load added by extenders.
*
* @return string[]
*/
protected function getRelationsToLoad(): array
{
@ -164,15 +171,34 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
}
/**
* 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 loadRelations(Collection $models, array $relations): void
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, 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) {
@ -194,7 +220,29 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
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);
}
}
@ -430,4 +478,16 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
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);
}
}

View File

@ -125,7 +125,7 @@ class ListDiscussionsController extends AbstractListController
$results = $results->getResults();
$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);
if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {
foreach ($results as $discussion) {

View File

@ -82,6 +82,10 @@ class ListGroupsController extends AbstractListController
$queryResults->areMoreResults() ? null : 0
);
return $queryResults->getResults();
$results = $queryResults->getResults();
$this->loadRelations($results, [], $request);
return $results;
}
}

View File

@ -79,7 +79,7 @@ class ListNotificationsController extends AbstractListController
$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();

View File

@ -108,7 +108,7 @@ class ListPostsController extends AbstractListController
$results = $results->getResults();
$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);
return $results;
}

View File

@ -109,7 +109,7 @@ class ListUsersController extends AbstractListController
$results = $results->getResults();
$this->loadRelations($results, $include);
$this->loadRelations($results, $include, $request);
return $results;
}

View File

@ -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.
@ -303,7 +304,27 @@ class ApiController implements ExtenderInterface
*/
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;
}
@ -375,6 +396,7 @@ class ApiController implements ExtenderInterface
}
AbstractSerializeController::setLoadRelations($this->controllerClass, $this->load);
AbstractSerializeController::setLoadRelationCallables($this->controllerClass, $this->loadCallables);
}
/**

View File

@ -803,6 +803,126 @@ class ApiControllerTest extends TestCase
$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