Skip to content

Commit 2c7daec

Browse files
authored
Merge pull request #21 from netiul/feature/extract-filter-processor
Extract filter processing logic to a seperate class
2 parents dacf453 + 1937cc1 commit 2c7daec

File tree

5 files changed

+306
-166
lines changed

5 files changed

+306
-166
lines changed

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
],
1818
"require": {
1919
"php": "^7.2 || ^8.0",
20+
"ext-json": "*",
2021
"ext-pdo": "*",
2122
"event-engine/php-persistence": "^0.9"
2223
},
2324
"require-dev": {
24-
"infection/infection": "^0.15.3",
25+
"infection/infection": "^0.26.6",
2526
"malukenho/docheader": "^0.1.8",
2627
"phpspec/prophecy": "^1.12.1",
2728
"phpstan/phpstan": "^0.12.48",
@@ -45,6 +46,10 @@
4546
"config": {
4647
"sort-packages": true,
4748
"platform": {
49+
},
50+
"allow-plugins": {
51+
"ocramius/package-versions": true,
52+
"infection/extension-installer": true
4853
}
4954
},
5055
"prefer-stable": true,

src/Filter/FilterClause.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\Filter;
13+
14+
final class FilterClause
15+
{
16+
private $clause;
17+
private $args;
18+
19+
public function __construct(?string $clause, array $args = [])
20+
{
21+
$this->clause = $clause;
22+
$this->args = $args;
23+
}
24+
25+
public function clause(): ?string
26+
{
27+
return $this->clause;
28+
}
29+
30+
public function args(): array
31+
{
32+
return $this->args;
33+
}
34+
}

src/Filter/FilterProcessor.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\Filter;
13+
14+
use EventEngine\DocumentStore\Filter\Filter;
15+
16+
interface FilterProcessor
17+
{
18+
public function process(Filter $filter): FilterClause;
19+
}
+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
/**
3+
* This file is part of the event-engine/php-postgres-document-store.
4+
* (c) 2019-2021 prooph software GmbH <contact@prooph.de>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace EventEngine\DocumentStore\Postgres\Filter;
13+
14+
use EventEngine\DocumentStore;
15+
use EventEngine\DocumentStore\Filter\Filter;
16+
use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException;
17+
use EventEngine\DocumentStore\Postgres\Exception\RuntimeException;
18+
19+
/**
20+
* Default filter processor class for converting a filter to a where clause.
21+
*/
22+
final class PostgresFilterProcessor implements FilterProcessor
23+
{
24+
/**
25+
* @var bool
26+
*/
27+
private $useMetadataColumns;
28+
29+
public function __construct(bool $useMetadataColumns = false)
30+
{
31+
$this->useMetadataColumns = $useMetadataColumns;
32+
}
33+
34+
public function process(Filter $filter): FilterClause
35+
{
36+
[$filterClause, $args] = $this->processFilter($filter);
37+
38+
return new FilterClause($filterClause, $args);
39+
}
40+
41+
/**
42+
* @param Filter $filter
43+
* @param int $argsCount
44+
* @return array
45+
*/
46+
private function processFilter(Filter $filter, int $argsCount = 0): array
47+
{
48+
if($filter instanceof DocumentStore\Filter\AnyFilter) {
49+
if($argsCount > 0) {
50+
throw new InvalidArgumentException('AnyFilter cannot be used together with other filters.');
51+
}
52+
return [null, [], $argsCount];
53+
}
54+
55+
if($filter instanceof DocumentStore\Filter\AndFilter) {
56+
[$filterA, $argsA, $argsCount] = $this->processFilter($filter->aFilter(), $argsCount);
57+
[$filterB, $argsB, $argsCount] = $this->processFilter($filter->bFilter(), $argsCount);
58+
return ["($filterA AND $filterB)", array_merge($argsA, $argsB), $argsCount];
59+
}
60+
61+
if($filter instanceof DocumentStore\Filter\OrFilter) {
62+
[$filterA, $argsA, $argsCount] = $this->processFilter($filter->aFilter(), $argsCount);
63+
[$filterB, $argsB, $argsCount] = $this->processFilter($filter->bFilter(), $argsCount);
64+
return ["($filterA OR $filterB)", array_merge($argsA, $argsB), $argsCount];
65+
}
66+
67+
switch (get_class($filter)) {
68+
case DocumentStore\Filter\DocIdFilter::class:
69+
/** @var DocumentStore\Filter\DocIdFilter $filter */
70+
return ["id = :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount];
71+
case DocumentStore\Filter\AnyOfDocIdFilter::class:
72+
/** @var DocumentStore\Filter\AnyOfDocIdFilter $filter */
73+
return $this->makeInClause('id', $filter->valList(), $argsCount);
74+
case DocumentStore\Filter\AnyOfFilter::class:
75+
/** @var DocumentStore\Filter\AnyOfFilter $filter */
76+
return $this->makeInClause($this->propToJsonPath($filter->prop()), $filter->valList(), $argsCount, $this->shouldJsonEncodeVal($filter->prop()));
77+
case DocumentStore\Filter\EqFilter::class:
78+
/** @var DocumentStore\Filter\EqFilter $filter */
79+
$prop = $this->propToJsonPath($filter->prop());
80+
return ["$prop = :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount];
81+
case DocumentStore\Filter\GtFilter::class:
82+
/** @var DocumentStore\Filter\GtFilter $filter */
83+
$prop = $this->propToJsonPath($filter->prop());
84+
return ["$prop > :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount];
85+
case DocumentStore\Filter\GteFilter::class:
86+
/** @var DocumentStore\Filter\GteFilter $filter */
87+
$prop = $this->propToJsonPath($filter->prop());
88+
return ["$prop >= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount];
89+
case DocumentStore\Filter\LtFilter::class:
90+
/** @var DocumentStore\Filter\LtFilter $filter */
91+
$prop = $this->propToJsonPath($filter->prop());
92+
return ["$prop < :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount];
93+
case DocumentStore\Filter\LteFilter::class:
94+
/** @var DocumentStore\Filter\LteFilter $filter */
95+
$prop = $this->propToJsonPath($filter->prop());
96+
return ["$prop <= :a$argsCount", ["a$argsCount" => $this->prepareVal($filter->val(), $filter->prop())], ++$argsCount];
97+
case DocumentStore\Filter\LikeFilter::class:
98+
/** @var DocumentStore\Filter\LikeFilter $filter */
99+
$prop = $this->propToJsonPath($filter->prop());
100+
$propParts = explode('->', $prop);
101+
$lastProp = array_pop($propParts);
102+
$prop = implode('->', $propParts) . '->>'.$lastProp;
103+
return ["$prop iLIKE :a$argsCount", ["a$argsCount" => $filter->val()], ++$argsCount];
104+
case DocumentStore\Filter\NotFilter::class:
105+
/** @var DocumentStore\Filter\NotFilter $filter */
106+
$innerFilter = $filter->innerFilter();
107+
108+
if (!$this->isPropFilter($innerFilter)) {
109+
throw new RuntimeException('Not filter cannot be combined with a non prop filter!');
110+
}
111+
112+
[$innerFilterStr, $args, $argsCount] = $this->processFilter($innerFilter, $argsCount);
113+
114+
if($innerFilter instanceof DocumentStore\Filter\AnyOfFilter || $innerFilter instanceof DocumentStore\Filter\AnyOfDocIdFilter) {
115+
if ($argsCount === 0) {
116+
return [
117+
str_replace(' 1 != 1 ', ' 1 = 1 ', $innerFilterStr),
118+
$args,
119+
$argsCount
120+
];
121+
}
122+
123+
$inPos = strpos($innerFilterStr, ' IN(');
124+
$filterStr = substr_replace($innerFilterStr, ' NOT IN(', $inPos, 4 /* " IN(" */);
125+
return [$filterStr, $args, $argsCount];
126+
}
127+
128+
return ["NOT $innerFilterStr", $args, $argsCount];
129+
case DocumentStore\Filter\InArrayFilter::class:
130+
/** @var DocumentStore\Filter\InArrayFilter $filter */
131+
$prop = $this->propToJsonPath($filter->prop());
132+
return ["$prop @> :a$argsCount", ["a$argsCount" => '[' . $this->prepareVal($filter->val(), $filter->prop()) . ']'], ++$argsCount];
133+
case DocumentStore\Filter\ExistsFilter::class:
134+
/** @var DocumentStore\Filter\ExistsFilter $filter */
135+
$prop = $this->propToJsonPath($filter->prop());
136+
$propParts = explode('->', $prop);
137+
$lastProp = trim(array_pop($propParts), "'");
138+
$parentProps = implode('->', $propParts);
139+
return ["JSONB_EXISTS($parentProps, '$lastProp')", [], $argsCount];
140+
default:
141+
throw new RuntimeException('Unsupported filter type. Got ' . get_class($filter));
142+
}
143+
}
144+
145+
private function makeInClause(string $prop, array $valList, int $argsCount, bool $jsonEncode = false): array
146+
{
147+
if ($valList === []) {
148+
return [' 1 != 1 ', [], 0];
149+
}
150+
$argList = [];
151+
$params = \implode(",", \array_map(function ($val) use (&$argsCount, &$argList, $jsonEncode) {
152+
$param = ":a$argsCount";
153+
$argList["a$argsCount"] = $jsonEncode? \json_encode($val) : $val;
154+
$argsCount++;
155+
return $param;
156+
}, $valList));
157+
158+
return ["$prop IN($params)", $argList, $argsCount];
159+
}
160+
161+
private function shouldJsonEncodeVal(string $prop): bool
162+
{
163+
if($this->useMetadataColumns && strpos($prop, 'metadata.') === 0) {
164+
return false;
165+
}
166+
167+
return true;
168+
}
169+
170+
private function propToJsonPath(string $field): string
171+
{
172+
if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) {
173+
return str_replace('metadata.', '', $field);
174+
}
175+
176+
return "doc->'" . str_replace('.', "'->'", $field) . "'";
177+
}
178+
179+
private function isPropFilter(Filter $filter): bool
180+
{
181+
switch (get_class($filter)) {
182+
case DocumentStore\Filter\AndFilter::class:
183+
case DocumentStore\Filter\OrFilter::class:
184+
case DocumentStore\Filter\NotFilter::class:
185+
return false;
186+
default:
187+
return true;
188+
}
189+
}
190+
191+
private function prepareVal($value, string $prop)
192+
{
193+
if(!$this->shouldJsonEncodeVal($prop)) {
194+
return $value;
195+
}
196+
197+
return \json_encode($value);
198+
}
199+
}

0 commit comments

Comments
 (0)