Skip to content

Commit 58151c7

Browse files
ngaspariStyleCIBot
andauthored
Extend for cf custom field search parser (#50)
* Apply fixes from StyleCI * implement CustomFieldSearchParser extend search parser for customfields usage * Apply fixes from StyleCI --------- Co-authored-by: StyleCI Bot <[email protected]> Co-authored-by: ngasparic <[email protected]>
1 parent 236bf07 commit 58151c7

8 files changed

+201
-27
lines changed

config/asseco-json-query-builder.php

+10-10
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
/**
2626
* Registered request parameters.
2727
*/
28-
'request_parameters' => [
28+
'request_parameters' => [
2929
SearchParameter::class,
3030
ReturnsParameter::class,
3131
OrderByParameter::class,
@@ -42,7 +42,7 @@
4242
* Registered operators/callbacks. Operator order matters!
4343
* Callbacks having more const OPERATOR characters must come before those with less.
4444
*/
45-
'operators' => [
45+
'operators' => [
4646
NotBetween::class,
4747
LessThanOrEqual::class,
4848
GreaterThanOrEqual::class,
@@ -57,7 +57,7 @@
5757
* Registered types. Generic type is the default one and should be used if
5858
* no special care for type value is needed.
5959
*/
60-
'types' => [
60+
'types' => [
6161
GenericType::class,
6262
BooleanType::class,
6363
],
@@ -76,7 +76,7 @@
7676
* Refined options for a single model.
7777
* Use if you want to enforce rules on a specific model without affecting globally all models.
7878
*/
79-
'model_options' => [
79+
'model_options' => [
8080

8181
/**
8282
* For real usage, use real models without quotes. This is only meant to show the available options.
@@ -95,33 +95,33 @@
9595
/**
9696
* Disable search on specific columns. Searching on forbidden columns will throw an exception.
9797
*/
98-
'forbidden_columns' => ['column', 'column2'],
98+
'forbidden_columns' => ['column', 'column2'],
9999
/**
100100
* Array of columns to order by in 'column => direction' format.
101101
* 'order-by' from query string takes precedence before these values.
102102
*/
103-
'order_by' => [
104-
'id' => 'asc',
103+
'order_by' => [
104+
'id' => 'asc',
105105
'created_at' => 'desc',
106106
],
107107
/**
108108
* List of columns to return. Return values forwarded within the request will
109109
* override these values. This acts as a 'SELECT /return only columns/' from.
110110
* By default, 'SELECT *' will be ran.
111111
*/
112-
'returns' => ['column', 'column2'],
112+
'returns' => ['column', 'column2'],
113113
/**
114114
* List of relations to load by default. These will be overridden if provided within query string.
115115
*/
116-
'relations' => ['rel1', 'rel2'],
116+
'relations' => ['rel1', 'rel2'],
117117

118118
/**
119119
* TBD
120120
* Some column names may be different on frontend than on backend.
121121
* It is possible to map such columns so that the true ORM
122122
* property stays hidden.
123123
*/
124-
'column_mapping' => [
124+
'column_mapping' => [
125125
'frontend_column' => 'backend_column',
126126
],
127127
],

src/CategorizedValues.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class CategorizedValues
1717
const IS_NULL = 'null';
1818
const IS_NOT_NULL = '!null';
1919

20-
protected SearchParser $searchParser;
20+
protected SearchParserInterface $searchParser;
2121

2222
public array $and = [];
2323
public array $andLike = [];
@@ -31,11 +31,11 @@ class CategorizedValues
3131
/**
3232
* CategorizedValues constructor.
3333
*
34-
* @param SearchParser $searchParser
34+
* @param SearchParserInterface $searchParser
3535
*
3636
* @throws Exceptions\JsonQueryBuilderException
3737
*/
38-
public function __construct(SearchParser $searchParser)
38+
public function __construct(SearchParserInterface $searchParser)
3939
{
4040
$this->searchParser = $searchParser;
4141

src/CustomFieldSearchParser.php

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Asseco\JsonQueryBuilder;
6+
7+
use Asseco\JsonQueryBuilder\Config\ModelConfig;
8+
use Asseco\JsonQueryBuilder\Config\OperatorsConfig;
9+
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
10+
use Asseco\JsonQueryBuilder\Traits\CleansValues;
11+
use Illuminate\Support\Facades\Config;
12+
13+
class CustomFieldSearchParser implements SearchParserInterface
14+
{
15+
use CleansValues;
16+
17+
/**
18+
* Constant by which values will be split within a single parameter. E.g. parameter=value1;value2.
19+
*/
20+
const VALUE_SEPARATOR = ';';
21+
22+
public string $column;
23+
private string $argument;
24+
public array $values;
25+
public string $type;
26+
public string $operator;
27+
28+
public string $cf_field_identificator = 'custom_field_id';
29+
public string $cf_field_value = '';
30+
31+
private ModelConfig $modelConfig;
32+
33+
/**
34+
* @param ModelConfig $modelConfig
35+
* @param OperatorsConfig $operatorsConfig
36+
* @param array $arguments
37+
*
38+
* @throws JsonQueryBuilderException
39+
*/
40+
public function __construct(ModelConfig $modelConfig, OperatorsConfig $operatorsConfig, array $arguments)
41+
{
42+
$this->modelConfig = $modelConfig;
43+
44+
foreach ($arguments as $col => $val) {
45+
if (str_contains($col, $this->cf_field_identificator)) {
46+
$this->cf_field_value = $val;
47+
} else {
48+
$this->column = $col;
49+
$this->argument = $val;
50+
}
51+
}
52+
53+
$this->checkForForbiddenColumns();
54+
55+
$this->operator = $this->parseOperator($operatorsConfig->getOperators(), $this->argument);
56+
$arguments = str_replace($this->operator, '', $this->argument);
57+
$this->values = $this->splitValues($arguments);
58+
$this->type = $this->getColumnType();
59+
}
60+
61+
/**
62+
* @param $operators
63+
* @param string $argument
64+
* @return string
65+
*
66+
* @throws JsonQueryBuilderException
67+
*/
68+
protected function parseOperator($operators, string $argument): string
69+
{
70+
foreach ($operators as $operator) {
71+
$argumentHasOperator = strpos($argument, $operator) !== false;
72+
73+
if (!$argumentHasOperator) {
74+
continue;
75+
}
76+
77+
return $operator;
78+
}
79+
80+
throw new JsonQueryBuilderException("No valid callback registered for $argument. Are you missing an operator?");
81+
}
82+
83+
/**
84+
* Split values by a given separator.
85+
*
86+
* Input: val1;val2
87+
*
88+
* Output: val1
89+
* val2
90+
*
91+
* @param string $values
92+
* @return array
93+
*
94+
* @throws JsonQueryBuilderException
95+
*/
96+
protected function splitValues(string $values): array
97+
{
98+
$valueArray = explode(self::VALUE_SEPARATOR, $values);
99+
$cleanedUpValues = $this->cleanValues($valueArray);
100+
101+
if (count($cleanedUpValues) < 1) {
102+
throw new JsonQueryBuilderException("Column '$this->column' is missing a value.");
103+
}
104+
105+
return $cleanedUpValues;
106+
}
107+
108+
/**
109+
* @return string
110+
*
111+
* @throws JsonQueryBuilderException
112+
*/
113+
protected function getColumnType(): string
114+
{
115+
$columns = $this->modelConfig->getModelColumns();
116+
117+
if (!array_key_exists($this->column, $columns)) {
118+
// TODO: integrate recursive column check for related models?
119+
return 'generic';
120+
}
121+
122+
return $columns[$this->column];
123+
}
124+
125+
/**
126+
* Check if global forbidden key is used.
127+
*
128+
* @throws JsonQueryBuilderException
129+
*/
130+
protected function checkForForbiddenColumns()
131+
{
132+
$forbiddenKeys = Config::get('asseco-json-query-builder.global_forbidden_columns');
133+
$forbiddenKeys = $this->modelConfig->getForbidden($forbiddenKeys);
134+
135+
if (in_array($this->column, $forbiddenKeys)) {
136+
throw new JsonQueryBuilderException("Searching by '$this->column' field is forbidden. Check the configuration if this is not a desirable behavior.");
137+
}
138+
}
139+
}

src/RequestParameters/SearchParameter.php

+19-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
namespace Asseco\JsonQueryBuilder\RequestParameters;
66

77
use Asseco\JsonQueryBuilder\Config\OperatorsConfig;
8+
use Asseco\JsonQueryBuilder\CustomFieldSearchParser;
89
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
910
use Asseco\JsonQueryBuilder\JsonQuery;
1011
use Asseco\JsonQueryBuilder\SearchCallbacks\AbstractCallback;
1112
use Asseco\JsonQueryBuilder\SearchParser;
13+
use Asseco\JsonQueryBuilder\SearchParserInterface;
1214
use Illuminate\Database\Eloquent\Builder;
1315
use Illuminate\Support\Str;
1416

@@ -17,6 +19,8 @@ class SearchParameter extends AbstractParameter
1719
const OR = '||';
1820
const AND = '&&';
1921

22+
const AND_INCLUSIVE_CF = '&&_INC_CF';
23+
2024
const LARAVEL_WHERE = 'where';
2125
const LARAVEL_OR_WHERE = 'orWhere';
2226

@@ -61,7 +65,14 @@ protected function makeQuery(Builder $builder, array $arguments, string $boolOpe
6165

6266
$functionName = $this->getQueryFunctionName($boolOperator);
6367

64-
if ($this->queryInitiatedByTopLevelBool($key, $value)) {
68+
if ($this->isTopLevelInclusiveCFOperator($key)) {
69+
// Custom fields custom search logic ..... both columns has to be in the same where clause (custom_field_id & search column)
70+
$builder->{$functionName}(function ($queryBuilder) use ($value) {
71+
$searchModel = new CustomFieldSearchParser($this->modelConfig, $this->operatorsConfig, $value);
72+
$this->appendSingle($queryBuilder, $this->operatorsConfig, $searchModel);
73+
});
74+
continue;
75+
} elseif ($this->queryInitiatedByTopLevelBool($key, $value)) {
6576
$builder->{$functionName}(function ($queryBuilder) use ($value) {
6677
// Recursion for inner keys which are &&/||
6778
$this->makeQuery($queryBuilder, $value);
@@ -87,6 +98,11 @@ protected function isTopLevelBoolOperator($key): bool
8798
return in_array($key, [self::OR, self::AND], true);
8899
}
89100

101+
protected function isTopLevelInclusiveCFOperator($key): bool
102+
{
103+
return in_array($key, [self::AND_INCLUSIVE_CF], true);
104+
}
105+
90106
/**
91107
* @param string $boolOperator
92108
* @return string
@@ -95,7 +111,7 @@ protected function isTopLevelBoolOperator($key): bool
95111
*/
96112
protected function getQueryFunctionName(string $boolOperator): string
97113
{
98-
if ($boolOperator === self::AND) {
114+
if ($boolOperator === self::AND || $boolOperator === self::AND_INCLUSIVE_CF) {
99115
return self::LARAVEL_WHERE;
100116
} elseif ($boolOperator === self::OR) {
101117
return self::LARAVEL_OR_WHERE;
@@ -186,7 +202,7 @@ protected function splitByBoolOperators($argument): array
186202
*
187203
* @throws JsonQueryBuilderException
188204
*/
189-
protected function appendSingle(Builder $builder, OperatorsConfig $operatorsConfig, SearchParser $searchParser): void
205+
protected function appendSingle(Builder $builder, OperatorsConfig $operatorsConfig, SearchParserInterface $searchParser): void
190206
{
191207
$callbackClassName = $operatorsConfig->getCallbackClassFromOperator($searchParser->operator);
192208

src/SearchCallbacks/AbstractCallback.php

+14-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
namespace Asseco\JsonQueryBuilder\SearchCallbacks;
66

77
use Asseco\JsonQueryBuilder\CategorizedValues;
8+
use Asseco\JsonQueryBuilder\CustomFieldSearchParser;
89
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
9-
use Asseco\JsonQueryBuilder\SearchParser;
10+
use Asseco\JsonQueryBuilder\SearchParserInterface;
1011
use Illuminate\Database\Eloquent\Builder;
1112
use Illuminate\Support\Facades\DB;
1213
use Illuminate\Support\Str;
@@ -15,7 +16,7 @@
1516
abstract class AbstractCallback
1617
{
1718
protected Builder $builder;
18-
protected SearchParser $searchParser;
19+
protected SearchParserInterface $searchParser;
1920
protected CategorizedValues $categorizedValues;
2021

2122
protected const DATE_FIELDS = [
@@ -26,11 +27,11 @@ abstract class AbstractCallback
2627
* AbstractCallback constructor.
2728
*
2829
* @param Builder $builder
29-
* @param SearchParser $searchParser
30+
* @param SearchParserInterface $searchParser
3031
*
3132
* @throws JsonQueryBuilderException
3233
*/
33-
public function __construct(Builder $builder, SearchParser $searchParser)
34+
public function __construct(Builder $builder, SearchParserInterface $searchParser)
3435
{
3536
$this->builder = $builder;
3637
$this->searchParser = $searchParser;
@@ -50,6 +51,7 @@ function (Builder $builder) {
5051
},
5152
function (Builder $builder) {
5253
$this->execute($builder, $this->searchParser->column, $this->categorizedValues);
54+
$this->checkExecuteForCustomfieldsParameter($builder);
5355
}
5456
);
5557
}
@@ -87,6 +89,7 @@ protected function appendRelations(Builder $builder, string $column, Categorized
8789
}
8890

8991
$this->execute($builder, $relatedColumns, $values);
92+
$this->checkExecuteForCustomfieldsParameter($builder);
9093
});
9194
}
9295

@@ -164,4 +167,11 @@ protected function getLikeOperator(): string
164167

165168
return 'LIKE';
166169
}
170+
171+
protected function checkExecuteForCustomfieldsParameter($builder)
172+
{
173+
if ($this->searchParser instanceof CustomFieldSearchParser) {
174+
$builder->where($this->searchParser->cf_field_identificator, '=', $this->searchParser->cf_field_value);
175+
}
176+
}
167177
}

src/SearchParser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Asseco\JsonQueryBuilder\Traits\CleansValues;
1111
use Illuminate\Support\Facades\Config;
1212

13-
class SearchParser
13+
class SearchParser implements SearchParserInterface
1414
{
1515
use CleansValues;
1616

src/SearchParserInterface.php

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Asseco\JsonQueryBuilder;
6+
7+
interface SearchParserInterface
8+
{
9+
}

0 commit comments

Comments
 (0)