Skip to content

Commit 4ef904c

Browse files
authored
Merge pull request #61 from MortalFlesh/feature/execute-function
Add execute function
2 parents 878929c + 5b3889e commit 4ef904c

File tree

10 files changed

+453
-36
lines changed

10 files changed

+453
-36
lines changed

src/ApiFilter.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct()
3131
$filterFactory = new FilterFactory();
3232
$this->functions = new Functions();
3333
$this->parser = new QueryParametersParser($filterFactory, $this->functions);
34-
$this->applicator = new FilterApplicator();
34+
$this->applicator = new FilterApplicator($this->functions);
3535
$this->functionCreator = new FunctionCreator($filterFactory);
3636

3737
if (class_exists('Doctrine\ORM\QueryBuilder')) {
@@ -63,7 +63,10 @@ public function __construct()
6363
*/
6464
public function parseFilters(array $queryParameters): FiltersInterface
6565
{
66-
return $this->parser->parse($queryParameters);
66+
$filters = $this->parser->parse($queryParameters);
67+
$this->applicator->setFilters($filters);
68+
69+
return $filters;
6770
}
6871

6972
/**
@@ -87,8 +90,12 @@ public function parseFilters(array $queryParameters): FiltersInterface
8790
* @throws ApiFilterExceptionInterface
8891
* @return mixed of type <T> - same as given filterable
8992
*/
90-
public function applyFilter(FilterInterface $filter, $filterable)
93+
public function applyFilter(FilterInterface $filter, $filterable, FiltersInterface $filters = null)
9194
{
95+
if ($filters) {
96+
$this->applicator->setFilters($filters);
97+
}
98+
9299
return $this->applicator->apply($filter, new Filterable($filterable))->getValue();
93100
}
94101

@@ -135,6 +142,8 @@ public function getPreparedValue(FilterInterface $filter, $filterable): array
135142
*/
136143
public function applyFilters(FiltersInterface $filters, $filterable)
137144
{
145+
$this->applicator->setFilters($filters);
146+
138147
return $this->applicator->applyAll($filters, new Filterable($filterable))->getValue();
139148
}
140149

@@ -251,4 +260,32 @@ public function registerFunction(string $functionName, array $parameters, callab
251260

252261
return $this;
253262
}
263+
264+
/**
265+
* Execute a function with parsed query parameters but without any implicit application
266+
* This allows you to bypass any applicator or not to implement one if you need to
267+
*
268+
* It will just parse filters and call a registered function with parsed filters
269+
*
270+
* @example
271+
* Executing a function, which bypasses ApiFilter and directly calls elastic search (see example of registerFunction)
272+
* $resultFromElastic = $apiFilter->executeFunction('elastic', $request->query->all(), null);
273+
*
274+
* @see ApiFilter::declareFunction()
275+
* @see ApiFilter::registerFunction()
276+
* @see ApiFilter::applyFunction() if you want apply function with applicators and get prepared values as well
277+
*
278+
* @param mixed $filterable of type <T> - this might not be supported by any applicator (if you don't use apply methods of ApiFilter)
279+
* @throws ApiFilterExceptionInterface
280+
* @return mixed of type <U> - the output of the registered function
281+
*/
282+
public function executeFunction(string $functionName, array $queryParameters, $filterable)
283+
{
284+
$filters = $this->parser->parse($queryParameters);
285+
$this->applicator->setFilters($filters);
286+
287+
return $this->functions
288+
->execute($functionName, $filters, new Filterable($filterable))
289+
->getValue();
290+
}
254291
}

src/Filters/Filters.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Lmc\ApiFilter\Filters;
44

55
use Lmc\ApiFilter\Applicator\ApplicatorInterface;
6+
use Lmc\ApiFilter\Assertion;
67
use Lmc\ApiFilter\Entity\Filterable;
78
use Lmc\ApiFilter\Filter\FilterIn;
89
use Lmc\ApiFilter\Filter\FilterInterface;
10+
use Lmc\ApiFilter\Filter\FunctionParameter;
911
use Lmc\ApiFilter\Service\FilterApplicator;
1012
use MF\Collection\Immutable\Generic\IList;
1113
use MF\Collection\Immutable\Generic\ListCollection;
@@ -102,4 +104,24 @@ public function toArray(): array
102104
{
103105
return $this->filters->toArray();
104106
}
107+
108+
public function filterByColumns(array $columns): FiltersInterface
109+
{
110+
$filtered = $this->filters->filter(function (FilterInterface $filter) use ($columns) {
111+
return in_array($filter->getColumn(), $columns, true);
112+
});
113+
114+
return self::from($filtered->toArray());
115+
}
116+
117+
public function getFunctionParameter(string $parameter): FunctionParameter
118+
{
119+
$functionParameter = $this->filters->firstBy(function (FilterInterface $filter) use ($parameter) {
120+
return $filter instanceof FunctionParameter && $filter->getColumn() === $parameter;
121+
});
122+
123+
Assertion::notNull($functionParameter, sprintf('Function parameter "%s" is missing.', $parameter));
124+
125+
return $functionParameter;
126+
}
105127
}

src/Filters/FiltersInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Lmc\ApiFilter\Applicator\ApplicatorInterface;
66
use Lmc\ApiFilter\Entity\Filterable;
77
use Lmc\ApiFilter\Filter\FilterInterface;
8+
use Lmc\ApiFilter\Filter\FunctionParameter;
89
use Lmc\ApiFilter\Service\FilterApplicator;
910
use MF\Collection\IEnumerable;
1011

@@ -21,5 +22,9 @@ public function hasFilter(FilterInterface $filter): bool;
2122

2223
public function addFilter(FilterInterface $filter): self;
2324

25+
public function filterByColumns(array $columns): self;
26+
27+
public function getFunctionParameter(string $parameter): FunctionParameter;
28+
2429
public function toArray(): array;
2530
}

src/Service/FilterApplicator.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,30 @@
33
namespace Lmc\ApiFilter\Service;
44

55
use Lmc\ApiFilter\Applicator\ApplicatorInterface;
6+
use Lmc\ApiFilter\Assertion;
67
use Lmc\ApiFilter\Entity\Filterable;
78
use Lmc\ApiFilter\Exception\UnsupportedFilterableException;
89
use Lmc\ApiFilter\Exception\UnsupportedFilterException;
10+
use Lmc\ApiFilter\Filter\FilterFunction;
911
use Lmc\ApiFilter\Filter\FilterIn;
1012
use Lmc\ApiFilter\Filter\FilterInterface;
1113
use Lmc\ApiFilter\Filter\FilterWithOperator;
14+
use Lmc\ApiFilter\Filter\FunctionParameter;
1215
use Lmc\ApiFilter\Filters\FiltersInterface;
1316
use MF\Collection\Mutable\Generic\PrioritizedCollection;
1417

1518
class FilterApplicator
1619
{
1720
/** @var PrioritizedCollection|ApplicatorInterface[] */
1821
private $applicators;
22+
/** @var Functions */
23+
private $functions;
24+
/** @var FiltersInterface */
25+
private $filters;
1926

20-
public function __construct()
27+
public function __construct(Functions $functions)
2128
{
29+
$this->functions = $functions;
2230
$this->applicators = new PrioritizedCollection(ApplicatorInterface::class);
2331
}
2432

@@ -27,14 +35,24 @@ public function registerApplicator(ApplicatorInterface $applicator, int $priorit
2735
$this->applicators->add($applicator, $priority);
2836
}
2937

38+
public function setFilters(FiltersInterface $filters): void
39+
{
40+
$this->filters = $filters;
41+
}
42+
3043
public function apply(FilterInterface $filter, Filterable $filterable): Filterable
3144
{
45+
Assertion::notNull($this->filters, 'Filters must be set before applying.');
3246
$applicator = $this->findApplicatorFor($filterable);
3347

3448
if ($filter instanceof FilterWithOperator) {
3549
return $applicator->applyFilterWithOperator($filter, $filterable);
3650
} elseif ($filter instanceof FilterIn) {
3751
return $applicator->applyFilterIn($filter, $filterable);
52+
} elseif ($filter instanceof FilterFunction) {
53+
return $applicator->applyFilterFunction($filter, $filterable, $this->getParametersForFunction($filter));
54+
} elseif ($filter instanceof FunctionParameter) {
55+
return $filterable;
3856
}
3957

4058
throw UnsupportedFilterException::forFilter($filter);
@@ -51,11 +69,26 @@ private function findApplicatorFor(Filterable $filterable): ApplicatorInterface
5169
throw UnsupportedFilterableException::forFilterable($filterable);
5270
}
5371

72+
private function getParametersForFunction(FilterFunction $filter): array
73+
{
74+
$parameters = [];
75+
foreach ($this->functions->getParametersFor($filter->getColumn()) as $parameter) {
76+
$parameters[] = $this->filters->getFunctionParameter($parameter);
77+
}
78+
79+
return $parameters;
80+
}
81+
5482
public function getPreparedValue(FilterInterface $filter, Filterable $filterable): array
5583
{
56-
return $this
57-
->findApplicatorFor($filterable)
58-
->getPreparedValue($filter);
84+
$applicator = $this->findApplicatorFor($filterable);
85+
86+
return $filter instanceof FilterFunction
87+
? $applicator->getPreparedValuesForFunction(
88+
$this->getParametersForFunction($filter),
89+
$this->functions->getParameterDefinitionsFor($filter->getColumn())
90+
)
91+
: $applicator->getPreparedValue($filter);
5992
}
6093

6194
public function applyAll(FiltersInterface $filters, Filterable $filterable): Filterable

src/Service/Functions.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace Lmc\ApiFilter\Service;
44

55
use Lmc\ApiFilter\Assertion;
6+
use Lmc\ApiFilter\Entity\Filterable;
67
use Lmc\ApiFilter\Entity\ParameterDefinition;
8+
use Lmc\ApiFilter\Filter\FilterInterface;
9+
use Lmc\ApiFilter\Filters\FiltersInterface;
710
use MF\Collection\Mutable\Generic\IMap;
811
use MF\Collection\Mutable\Generic\Map;
912

@@ -120,4 +123,36 @@ private function sort(array $array): array
120123

121124
return $array;
122125
}
126+
127+
/** @return ParameterDefinition[] */
128+
public function getParameterDefinitionsFor(string $functionName): array
129+
{
130+
$this->assertRegistered($functionName);
131+
132+
return $this->parameterDefinitions[$functionName];
133+
}
134+
135+
/** @param FiltersInterface|FilterInterface[] $filters */
136+
public function execute(string $functionName, FiltersInterface $filters, Filterable $filterable): Filterable
137+
{
138+
$this->assertRegistered($functionName);
139+
$function = $this->functions[$functionName];
140+
$parameters = $this->functionParameters[$functionName];
141+
142+
$functionParameters = $filters->filterByColumns($parameters);
143+
$this->assertFiltersByParameters($parameters, $functionParameters);
144+
145+
$applied = $function($filterable->getValue(), ...$functionParameters);
146+
147+
return new Filterable($applied);
148+
}
149+
150+
private function assertFiltersByParameters(array $parameters, FiltersInterface $filterByParameters): void
151+
{
152+
Assertion::same(
153+
count($filterByParameters),
154+
count($parameters),
155+
'There are not filters (%s) for parameters (%s).'
156+
);
157+
}
123158
}

tests/ApiFilterRegisterFunctionTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Lmc\ApiFilter\Applicator\SqlApplicator;
77
use Lmc\ApiFilter\Constant\Priority;
88
use Lmc\ApiFilter\Exception\ApiFilterExceptionInterface;
9+
use Lmc\ApiFilter\Filter\FunctionParameter;
10+
use Lmc\ApiFilter\Fixture\SimpleClient;
911

1012
class ApiFilterRegisterFunctionTest extends AbstractTestCase
1113
{
@@ -22,6 +24,43 @@ protected function setUp(): void
2224
$this->apiFilter->registerApplicator(new SqlApplicator(), Priority::HIGHEST);
2325
}
2426

27+
/**
28+
* @test
29+
* @dataProvider provideSqlQueryParameters
30+
*/
31+
public function shouldRegisterAndExecuteFunctionWhichBypassApiFilter(array $queryParameters): void
32+
{
33+
$client = new SimpleClient(['data' => 'some data']);
34+
$expected = [
35+
'data' => 'some data',
36+
'query' => 'SELECT * FROM table',
37+
];
38+
39+
$result = $this->apiFilter
40+
->registerFunction(
41+
'sql',
42+
['query'],
43+
function (SimpleClient $filterable, FunctionParameter $query) {
44+
return $filterable->query($query->getValue()->getValue());
45+
}
46+
)
47+
->executeFunction('sql', $queryParameters, $client);
48+
49+
$this->assertSame($expected, $result);
50+
}
51+
52+
public function provideSqlQueryParameters(): array
53+
{
54+
return [
55+
// queryParameters
56+
'implicit - single value' => [['sql' => 'SELECT * FROM table']],
57+
'explicit - tuple' => [['(function,query)' => '(sql, "SELECT * FROM table")']],
58+
'implicit - single values' => [['query' => 'SELECT * FROM table']],
59+
'explicit - single values' => [['function' => ['sql'], 'query' => 'SELECT * FROM table']],
60+
'explicit - filter' => [['filter' => ['(sql, SELECT * FROM table)']]],
61+
];
62+
}
63+
2564
/**
2665
* @test
2766
*/

tests/ApiFilterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public function shouldNotApplyFilterOnInvalidFilterable(
223223
$this->expectException(ApiFilterExceptionInterface::class);
224224
$this->expectExceptionMessage($expectedMessage);
225225

226-
$this->apiFilter->applyFilter($filter, $filterable);
226+
$this->apiFilter->applyFilter($filter, $filterable, Filters::from([$filter]));
227227
}
228228

229229
public function provideNotSupportedFilterable(): array

tests/Fixture/SimpleClient.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Lmc\ApiFilter\Fixture;
4+
5+
class SimpleClient
6+
{
7+
/** @var array */
8+
private $data;
9+
10+
public function __construct(array $data)
11+
{
12+
$this->data = $data;
13+
}
14+
15+
public function query(string $query): array
16+
{
17+
$data = $this->data;
18+
$data['query'] = $query;
19+
20+
return $data;
21+
}
22+
}

0 commit comments

Comments
 (0)