Skip to content

Commit bdf9426

Browse files
committed
resolve view includes with eager loading for optimize queries with nested includes
1 parent f00862e commit bdf9426

File tree

9 files changed

+86
-29
lines changed

9 files changed

+86
-29
lines changed

src/actions/HasIncludes.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace insolita\fractal\actions;
4+
5+
use League\Fractal\TransformerAbstract;
6+
use Yii;
7+
use yii\db\ActiveQueryInterface;
8+
use function explode;
9+
use function in_array;
10+
11+
trait HasIncludes
12+
{
13+
/**
14+
* Eager loading for included relations
15+
* @param \yii\db\ActiveQueryInterface|\yii\db\ActiveQuery $query
16+
* @return \yii\db\ActiveQueryInterface
17+
*/
18+
protected function prepareIncludeQuery(ActiveQueryInterface $query):ActiveQueryInterface
19+
{
20+
if (!Yii::$app->request->isGet) {
21+
return $query;
22+
}
23+
if (!$this->transformer instanceof TransformerAbstract) {
24+
return $query;
25+
}
26+
$defaultIncludes = $this->transformer->getDefaultIncludes();
27+
$allowedIncludes = $this->transformer->getAvailableIncludes();
28+
$requestedIncludes = $this->controller->manager->getRequestedIncludes();
29+
$validIncludes = array_filter($requestedIncludes, function ($value) use ($allowedIncludes) {
30+
$baseRelation = explode('.', $value)[0];
31+
return in_array($baseRelation, $allowedIncludes, true);
32+
});
33+
$include = array_merge($defaultIncludes, $validIncludes);
34+
//@TODO: ?validate if included relations existed ?
35+
return empty($include)? $query : $query->with($include);
36+
}
37+
}

src/actions/JsonApiAction.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
class JsonApiAction extends Action
2727
{
2828
use HasResourceBodyParams;
29+
use HasIncludes;
2930
/**
3031
* @var \insolita\fractal\JsonApiController $controller
3132
*/
@@ -106,9 +107,11 @@ protected function findModel($id)
106107
$condition = $this->findModelCondition($id);
107108

108109
if (!empty($condition)) {
109-
$model = $modelClass::findOne($condition);
110+
$query = $this->prepareIncludeQuery($modelClass::find());
111+
$model = $query->where($condition)->limit(1)->one();
110112
}
111113

114+
112115
if (isset($model)) {
113116
return $model;
114117
}

src/actions/ListAction.php

+2-23
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
use insolita\fractal\exceptions\ValidationException;
66
use insolita\fractal\providers\CursorActiveDataProvider;
77
use insolita\fractal\providers\JsonApiActiveDataProvider;
8-
use League\Fractal\TransformerAbstract;
98
use Yii;
109
use yii\base\InvalidConfigException;
1110
use yii\db\ActiveQueryInterface;
12-
use function array_intersect;
13-
use function array_merge;
1411

1512
/**
1613
* Handler for routes GET /resource
@@ -93,24 +90,6 @@ protected function prepareDataFilter($requestParams)
9390
return null;
9491
}
9592

96-
/**
97-
* Eager loading for included relations
98-
* @param \yii\db\ActiveQueryInterface|\yii\db\ActiveQuery $query
99-
* @return \yii\db\ActiveQueryInterface
100-
*/
101-
protected function prepareIncludeQuery(ActiveQueryInterface $query):ActiveQueryInterface
102-
{
103-
if (!$this->transformer instanceof TransformerAbstract) {
104-
return $query;
105-
}
106-
$defaultIncludes = $this->transformer->getDefaultIncludes();
107-
$allowedIncludes = $this->transformer->getAvailableIncludes();
108-
$requestedIncludes = $this->controller->manager->getRequestedIncludes();
109-
$include = array_merge($defaultIncludes, array_intersect($allowedIncludes, $requestedIncludes));
110-
//@TODO: ?validate if included relations existed ?
111-
return empty($include)? $query : $query->with($include);
112-
}
113-
11493
/**
11594
* Add condition for parent model restriction if needed
11695
* @param \yii\db\ActiveQueryInterface $query
@@ -148,9 +127,9 @@ protected function makeDataProvider()
148127

149128
/* @var $modelClass \yii\db\BaseActiveRecord */
150129
$modelClass = $this->modelClass;
130+
$query = $this->prepareParentQuery($modelClass::find());
131+
$query = $this->prepareIncludeQuery($query);
151132

152-
$query = $this->prepareIncludeQuery($modelClass::find());
153-
$query = $this->prepareParentQuery($query);
154133

155134
if (!empty($filter)) {
156135
$query->andWhere($filter);

src/actions/ViewAction.php

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function run($id)
3434
{
3535
$model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id);
3636

37+
3738
if ($this->checkAccess) {
3839
call_user_func($this->checkAccess, $this->id, $model);
3940
}

tests/testapp/config/api.php

+2
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@
9393
'DELETE /categories/<categoryId:\d+>/posts/<id:\d+>' => 'post/delete-for-category',
9494
'OPTIONS /posts' => 'post/options',
9595
'GET,HEAD /posts' => 'post/list',
96+
'GET,HEAD /comments' => 'comment/list',
9697
'POST /posts' => 'post/create',
9798
'POST /posts2' => 'post/create2',
9899
'POST /comments' => 'comment/create',
99100
'GET,HEAD /posts/<id:\d+>' => 'post/view',
101+
'GET,HEAD /comments/<id:\d+>' => 'comment/view',
100102
'GET,HEAD /posts/<id:\d+>/relationships/author' => 'post/related-author',
101103
'GET,HEAD /posts/<id:\d+>/relationships/category' => 'post/related-category',
102104
'DELETE /posts/<id:\d+>/relationships/category' => 'post/delete-related-category',

tests/testapp/controllers/CommentController.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?php
22

3-
namespace testapp\controllers;
3+
namespace app\controllers;
44

55
use app\models\Comment;
66
use insolita\fractal\ActiveJsonApiController;
7-
use testapp\transformers\CommentTransformer;
7+
use app\transformers\CommentTransformer;
88

99
class CommentController extends ActiveJsonApiController
1010
{

tests/testapp/models/Comment.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ public function rules()
5252

5353
public function getUser()
5454
{
55-
return $this->hasOne(User::class, ['user_id' => 'id']);
55+
return $this->hasOne(User::class, ['id' => 'user_id']);
5656
}
5757

5858
public function getPost()
5959
{
60-
return $this->hasOne(Post::class, ['post_id'=>'id']);
60+
return $this->hasOne(Post::class, ['id'=>'post_id']);
6161
}
6262
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
<?php
22

3-
namespace testapp\transformers;
3+
namespace app\transformers;
44

55
use app\models\Comment;
66
use League\Fractal\TransformerAbstract;
77

88
class CommentTransformer extends TransformerAbstract
99
{
10+
public $availableIncludes = ['post'];
11+
1012
public function transform(Comment $comment):array
1113
{
1214
return $comment->getAttributes();
1315
}
16+
17+
public function includePost(Comment $comment)
18+
{
19+
$transformer = new PostTransformer();
20+
return $this->item($comment->post, $transformer, 'posts');
21+
}
1422
}

tests/tests_phpstorm/comments.http

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
### Index
2+
GET http://127.0.0.1:8400/comments?page[size]=5
3+
Accept: application/vnd.api+json
4+
5+
### Index with include
6+
GET http://127.0.0.1:8400/comments?page[size]=2&include=post
7+
Accept: application/vnd.api+json
8+
9+
### Index with bad include
10+
GET http://127.0.0.1:8400/comments?page[size]=2&include=tags
11+
Accept: application/vnd.api+json
12+
13+
### Index with include nested
14+
GET http://127.0.0.1:8400/comments?page[size]=2&include=post,post.author
15+
Accept: application/vnd.api+json
16+
17+
### Index with bad nested include
18+
GET http://127.0.0.1:8400/comments?page[size]=2&include=post,post.tags
19+
Accept: application/vnd.api+json
20+
21+
### View with include
22+
GET http://127.0.0.1:8400/comment/2?include=post
23+
Accept: application/vnd.api+json
24+
25+
### View with include nested
26+
GET http://127.0.0.1:8400/comment/2?include=post.author,post.category
27+
Accept: application/vnd.api+json

0 commit comments

Comments
 (0)