From 62b92ba02e54da4be714be1ccda79d4e3a99e035 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Mon, 1 Nov 2021 10:45:02 +0100 Subject: [PATCH] feat: Create `loadWhere` relations extender (#3116) --- .../AbstractSerializeController.php | 74 ++++++++++- .../Controller/ListDiscussionsController.php | 2 +- .../Api/Controller/ListGroupsController.php | 6 +- .../ListNotificationsController.php | 2 +- .../Api/Controller/ListPostsController.php | 2 +- .../Api/Controller/ListUsersController.php | 2 +- framework/core/src/Extend/ApiController.php | 24 +++- .../extenders/ApiControllerTest.php | 120 ++++++++++++++++++ 8 files changed, 219 insertions(+), 13 deletions(-) diff --git a/framework/core/src/Api/Controller/AbstractSerializeController.php b/framework/core/src/Api/Controller/AbstractSerializeController.php index a0a459da4..ce89c1d23 100644 --- a/framework/core/src/Api/Controller/AbstractSerializeController.php +++ b/framework/core/src/Api/Controller/AbstractSerializeController.php @@ -88,10 +88,15 @@ abstract class AbstractSerializeController implements RequestHandlerInterface protected static $beforeSerializationCallbacks = []; /** - * @var array + * @var string[] */ protected static $loadRelations = []; + /** + * @var array + */ + 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 */ - 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); + } } diff --git a/framework/core/src/Api/Controller/ListDiscussionsController.php b/framework/core/src/Api/Controller/ListDiscussionsController.php index 9bc70b7e2..7bfef5acd 100644 --- a/framework/core/src/Api/Controller/ListDiscussionsController.php +++ b/framework/core/src/Api/Controller/ListDiscussionsController.php @@ -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) { diff --git a/framework/core/src/Api/Controller/ListGroupsController.php b/framework/core/src/Api/Controller/ListGroupsController.php index 5db7b661b..8128c0de8 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -82,6 +82,10 @@ class ListGroupsController extends AbstractListController $queryResults->areMoreResults() ? null : 0 ); - return $queryResults->getResults(); + $results = $queryResults->getResults(); + + $this->loadRelations($results, [], $request); + + return $results; } } diff --git a/framework/core/src/Api/Controller/ListNotificationsController.php b/framework/core/src/Api/Controller/ListNotificationsController.php index 2ced62813..aff1f54d6 100644 --- a/framework/core/src/Api/Controller/ListNotificationsController.php +++ b/framework/core/src/Api/Controller/ListNotificationsController.php @@ -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(); diff --git a/framework/core/src/Api/Controller/ListPostsController.php b/framework/core/src/Api/Controller/ListPostsController.php index 70fec3483..f35d5f57a 100644 --- a/framework/core/src/Api/Controller/ListPostsController.php +++ b/framework/core/src/Api/Controller/ListPostsController.php @@ -108,7 +108,7 @@ class ListPostsController extends AbstractListController $results = $results->getResults(); - $this->loadRelations($results, $include); + $this->loadRelations($results, $include, $request); return $results; } diff --git a/framework/core/src/Api/Controller/ListUsersController.php b/framework/core/src/Api/Controller/ListUsersController.php index 1ff8b0274..c48ca37dc 100644 --- a/framework/core/src/Api/Controller/ListUsersController.php +++ b/framework/core/src/Api/Controller/ListUsersController.php @@ -109,7 +109,7 @@ class ListUsersController extends AbstractListController $results = $results->getResults(); - $this->loadRelations($results, $include); + $this->loadRelations($results, $include, $request); return $results; } diff --git a/framework/core/src/Extend/ApiController.php b/framework/core/src/Extend/ApiController.php index 251bed9d8..78cf72c6f 100644 --- a/framework/core/src/Extend/ApiController.php +++ b/framework/core/src/Extend/ApiController.php @@ -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); } /** diff --git a/framework/core/tests/integration/extenders/ApiControllerTest.php b/framework/core/tests/integration/extenders/ApiControllerTest.php index c23a7f667..de0e4daa4 100644 --- a/framework/core/tests/integration/extenders/ApiControllerTest.php +++ b/framework/core/tests/integration/extenders/ApiControllerTest.php @@ -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