Skip to content

Commit 9c17c31

Browse files
committed
Add filter for relation
1 parent 6ad46f6 commit 9c17c31

File tree

4 files changed

+188
-4
lines changed

4 files changed

+188
-4
lines changed

src/Struct/FilterStruct.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@
22

33
namespace LIQRGV\QueryFilter\Struct;
44

5+
use Illuminate\Database\Eloquent\Builder;
6+
57
class FilterStruct {
68
private static $OPERATOR_MAPPING = [
79
"is" => "=",
810
"!is" => "!=",
911
];
1012

11-
private static $LOGICAL_OR = "|";
13+
private static $LOGICAL_OR_FLAG = "|";
14+
private static $NOT_FLAG = "!";
1215

1316
private static $WHERE_QUERY_MAPPING = [
1417
"in" => "whereIn",
1518
"!in" => "whereNotIn",
1619
"between" => "whereBetween",
1720
];
1821

22+
private static $RELATION_FLAG_MAPPING = [
23+
true => "whereHas",
24+
false => "whereDoesntHave",
25+
];
26+
1927
public $fieldName;
2028
public $operator;
2129
public $value;
@@ -27,8 +35,8 @@ public function __construct($fieldName, $operator, $value) {
2735
}
2836

2937
public function apply($object) {
30-
if (strpos($this->fieldName, self::$LOGICAL_OR)) {
31-
$fieldNames = explode(self::$LOGICAL_OR, $this->fieldName);
38+
if (strpos($this->fieldName, self::$LOGICAL_OR_FLAG)) {
39+
$fieldNames = explode(self::$LOGICAL_OR_FLAG, $this->fieldName);
3240
$fieldNameCount = count($fieldNames);
3341
$subquery = function ($query) use ($fieldNames, $fieldNameCount) {
3442
for ($fieldIndex = 0; $fieldIndex < $fieldNameCount; $fieldIndex++) {
@@ -42,7 +50,10 @@ public function apply($object) {
4250
return $this->_apply($object, $this->fieldName, 'and');
4351
}
4452

45-
private function _apply($object, $fieldName, $boolean) {
53+
private function _apply(Builder $object, $fieldName, $boolean, $isChild = false) {
54+
if ($this->isRelation($fieldName)) {
55+
return $this->_applyRelation($object, $fieldName, $boolean, $isChild);
56+
}
4657
if (array_key_exists($this->operator, self::$WHERE_QUERY_MAPPING)) {
4758
$whereQuery = self::$WHERE_QUERY_MAPPING[$this->operator];
4859
$value = explode(",", $this->value);
@@ -60,4 +71,28 @@ private function transformOperator($rawOperator) {
6071

6172
return $rawOperator;
6273
}
74+
75+
private function _applyRelation(Builder $object, $fieldName, $boolean, $isChild)
76+
{
77+
[$relation, $other] = explode('.', $fieldName, 2);
78+
79+
$relationQuery = "whereHas";
80+
if (!$isChild) {
81+
$operatorType = $this->operator[0] !== self::$NOT_FLAG;
82+
$relationQuery = self::$RELATION_FLAG_MAPPING[$operatorType];
83+
}
84+
85+
if ($boolean === 'or') {
86+
$relationQuery = "or" . ucfirst($relationQuery);
87+
}
88+
89+
return $object->$relationQuery($relation, function (Builder $relation) use ($other) {
90+
return $this->_apply($relation, $other, 'and');
91+
});
92+
}
93+
94+
private function isRelation($fieldName)
95+
{
96+
return strpos($fieldName, '.') !== false;
97+
}
6398
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace LIQRGV\QueryFilter\Mocks\RelationMocks;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use LIQRGV\QueryFilter\Mocks\MockModel;
7+
8+
class MockModelWithRelationMany extends Model
9+
{
10+
function mockModels()
11+
{
12+
return $this->hasMany(MockModel::class);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace LIQRGV\QueryFilter\Mocks\RelationMocks;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use LIQRGV\QueryFilter\Mocks\MockModel;
7+
8+
class MockModelWithRelationOne extends Model
9+
{
10+
function mockModel()
11+
{
12+
return $this->hasOne(MockModel::class);
13+
}
14+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
namespace Tests\LIQRGV\QueryFilter;
4+
5+
use Illuminate\Database\Query\Builder;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Routing\Route;
8+
use Illuminate\Support\Facades\Config;
9+
use LIQRGV\QueryFilter\Exception\ModelNotFoundException;
10+
use LIQRGV\QueryFilter\Exception\NotModelException;
11+
use LIQRGV\QueryFilter\Mocks\MockModelController;
12+
use LIQRGV\QueryFilter\Mocks\RelationMocks\MockModelWithRelationOne;
13+
use LIQRGV\QueryFilter\RequestParser;
14+
use Symfony\Component\HttpFoundation\ParameterBag;
15+
16+
class RequestParserRelationTest extends TestCase
17+
{
18+
function testFilterRelationOne()
19+
{
20+
$uri = 'some_model';
21+
$controllerClass = MockModelController::class;
22+
$query = new ParameterBag([
23+
"filter" => [
24+
"mockModel.id" => [
25+
"is" => 2
26+
]
27+
]
28+
]);
29+
$requestParserOptions = [
30+
];
31+
32+
$request = $this->createControllerRequest($uri, $controllerClass, $query, $requestParserOptions);
33+
34+
$requestParser = new RequestParser($request);
35+
$requestParser->setModel(MockModelWithRelationOne::class);
36+
$builder = $requestParser->getBuilder();
37+
38+
$query = $builder->getQuery();
39+
$this->assertEquals("mock_model_with_relation_ones", $query->from);
40+
41+
// assert relation first
42+
$this->assertEquals("Column", $builder->getQuery()->wheres[0]["query"]->wheres[0]["type"]);
43+
$this->assertEquals("mock_model_with_relation_ones.id", $builder->getQuery()->wheres[0]["query"]->wheres[0]["first"]);
44+
$this->assertEquals("=", $builder->getQuery()->wheres[0]["query"]->wheres[0]["operator"]);
45+
$this->assertEquals("mock_models.mock_model_with_relation_one_id", $builder->getQuery()->wheres[0]["query"]->wheres[0]["second"]);
46+
$this->assertEquals("and", $builder->getQuery()->wheres[0]["query"]->wheres[0]["boolean"]);
47+
48+
// assert relation query
49+
$this->assertEquals("Basic", $builder->getQuery()->wheres[0]["query"]->wheres[1]["type"]);
50+
$this->assertEquals("id", $builder->getQuery()->wheres[0]["query"]->wheres[1]["column"]);
51+
$this->assertEquals("=", $builder->getQuery()->wheres[0]["query"]->wheres[1]["operator"]);
52+
$this->assertEquals("2", $builder->getQuery()->wheres[0]["query"]->wheres[1]["value"]);
53+
$this->assertEquals("and", $builder->getQuery()->wheres[0]["query"]->wheres[1]["boolean"]);
54+
}
55+
56+
function testFilterOrRelationOne()
57+
{
58+
$uri = 'some_model';
59+
$controllerClass = MockModelController::class;
60+
$query = new ParameterBag([
61+
"filter" => [
62+
"mockModel.id|mockModel.other_attr" => [
63+
"is" => 2
64+
]
65+
]
66+
]);
67+
$requestParserOptions = [];
68+
69+
$request = $this->createControllerRequest($uri, $controllerClass, $query, $requestParserOptions);
70+
71+
$requestParser = new RequestParser($request);
72+
$requestParser->setModel(MockModelWithRelationOne::class);
73+
$builder = $requestParser->getBuilder();
74+
75+
$query = $builder->getQuery();
76+
$this->assertEquals("mock_model_with_relation_ones", $query->from);
77+
78+
// assert nested query
79+
$this->assertEquals("Nested", $builder->getQuery()->wheres[0]["type"]);
80+
$this->assertEquals("and", $builder->getQuery()->wheres[0]["boolean"]);
81+
82+
/** @var Builder $nestedQuery */
83+
$nestedQuery = $builder->getQuery()->wheres[0]["query"];
84+
85+
$this->assertEquals("Exists", $nestedQuery->wheres[0]["type"]);
86+
$this->assertEquals("Exists", $nestedQuery->wheres[1]["type"]);
87+
$this->assertEquals("or", $nestedQuery->wheres[0]["boolean"]);
88+
$this->assertEquals("or", $nestedQuery->wheres[1]["boolean"]);
89+
90+
$firstInnerQuery = $nestedQuery->wheres[0]["query"];
91+
$secondInnerQuery = $nestedQuery->wheres[1]["query"];
92+
93+
// assert first relation
94+
$this->assertEquals("Column", $firstInnerQuery->wheres[0]["type"]);
95+
$this->assertEquals("mock_model_with_relation_ones.id", $firstInnerQuery->wheres[0]["first"]);
96+
$this->assertEquals("=", $firstInnerQuery->wheres[0]["operator"]);
97+
$this->assertEquals("mock_models.mock_model_with_relation_one_id", $firstInnerQuery->wheres[0]["second"]);
98+
$this->assertEquals("and", $firstInnerQuery->wheres[0]["boolean"]);
99+
100+
// assert first relation query field
101+
$this->assertEquals("Basic", $firstInnerQuery->wheres[1]["type"]);
102+
$this->assertEquals("id", $firstInnerQuery->wheres[1]["column"]);
103+
$this->assertEquals("=", $firstInnerQuery->wheres[1]["operator"]);
104+
$this->assertEquals("2", $firstInnerQuery->wheres[1]["value"]);
105+
$this->assertEquals("and", $firstInnerQuery->wheres[1]["boolean"]);
106+
107+
// assert second relation
108+
$this->assertEquals("Column", $secondInnerQuery->wheres[0]["type"]);
109+
$this->assertEquals("mock_model_with_relation_ones.id", $secondInnerQuery->wheres[0]["first"]);
110+
$this->assertEquals("=", $secondInnerQuery->wheres[0]["operator"]);
111+
$this->assertEquals("mock_models.mock_model_with_relation_one_id", $secondInnerQuery->wheres[0]["second"]);
112+
$this->assertEquals("and", $secondInnerQuery->wheres[0]["boolean"]);
113+
114+
// assert second relation query field
115+
$this->assertEquals("Basic", $secondInnerQuery->wheres[1]["type"]);
116+
$this->assertEquals("other_attr", $secondInnerQuery->wheres[1]["column"]);
117+
$this->assertEquals("=", $secondInnerQuery->wheres[1]["operator"]);
118+
$this->assertEquals("2", $secondInnerQuery->wheres[1]["value"]);
119+
$this->assertEquals("and", $secondInnerQuery->wheres[1]["boolean"]);
120+
}
121+
}

0 commit comments

Comments
 (0)