Skip to content

Commit

Permalink
Extend for cf custom field search parser (#50)
Browse files Browse the repository at this point in the history
* Apply fixes from StyleCI

* implement CustomFieldSearchParser

extend search parser for customfields usage

* Apply fixes from StyleCI

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: ngasparic <Nikola.Gasparic@asseco-see.hr>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent 236bf07 commit 58151c7
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 27 deletions.
20 changes: 10 additions & 10 deletions config/asseco-json-query-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/**
* Registered request parameters.
*/
'request_parameters' => [
'request_parameters' => [
SearchParameter::class,
ReturnsParameter::class,
OrderByParameter::class,
Expand All @@ -42,7 +42,7 @@
* Registered operators/callbacks. Operator order matters!
* Callbacks having more const OPERATOR characters must come before those with less.
*/
'operators' => [
'operators' => [
NotBetween::class,
LessThanOrEqual::class,
GreaterThanOrEqual::class,
Expand All @@ -57,7 +57,7 @@
* Registered types. Generic type is the default one and should be used if
* no special care for type value is needed.
*/
'types' => [
'types' => [
GenericType::class,
BooleanType::class,
],
Expand All @@ -76,7 +76,7 @@
* Refined options for a single model.
* Use if you want to enforce rules on a specific model without affecting globally all models.
*/
'model_options' => [
'model_options' => [

/**
* For real usage, use real models without quotes. This is only meant to show the available options.
Expand All @@ -95,33 +95,33 @@
/**
* Disable search on specific columns. Searching on forbidden columns will throw an exception.
*/
'forbidden_columns' => ['column', 'column2'],
'forbidden_columns' => ['column', 'column2'],
/**
* Array of columns to order by in 'column => direction' format.
* 'order-by' from query string takes precedence before these values.
*/
'order_by' => [
'id' => 'asc',
'order_by' => [
'id' => 'asc',
'created_at' => 'desc',
],
/**
* List of columns to return. Return values forwarded within the request will
* override these values. This acts as a 'SELECT /return only columns/' from.
* By default, 'SELECT *' will be ran.
*/
'returns' => ['column', 'column2'],
'returns' => ['column', 'column2'],
/**
* List of relations to load by default. These will be overridden if provided within query string.
*/
'relations' => ['rel1', 'rel2'],
'relations' => ['rel1', 'rel2'],

/**
* TBD
* Some column names may be different on frontend than on backend.
* It is possible to map such columns so that the true ORM
* property stays hidden.
*/
'column_mapping' => [
'column_mapping' => [
'frontend_column' => 'backend_column',
],
],
Expand Down
6 changes: 3 additions & 3 deletions src/CategorizedValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CategorizedValues
const IS_NULL = 'null';
const IS_NOT_NULL = '!null';

protected SearchParser $searchParser;
protected SearchParserInterface $searchParser;

public array $and = [];
public array $andLike = [];
Expand All @@ -31,11 +31,11 @@ class CategorizedValues
/**
* CategorizedValues constructor.
*
* @param SearchParser $searchParser
* @param SearchParserInterface $searchParser
*
* @throws Exceptions\JsonQueryBuilderException
*/
public function __construct(SearchParser $searchParser)
public function __construct(SearchParserInterface $searchParser)
{
$this->searchParser = $searchParser;

Expand Down
139 changes: 139 additions & 0 deletions src/CustomFieldSearchParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

namespace Asseco\JsonQueryBuilder;

use Asseco\JsonQueryBuilder\Config\ModelConfig;
use Asseco\JsonQueryBuilder\Config\OperatorsConfig;
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
use Asseco\JsonQueryBuilder\Traits\CleansValues;
use Illuminate\Support\Facades\Config;

class CustomFieldSearchParser implements SearchParserInterface
{
use CleansValues;

/**
* Constant by which values will be split within a single parameter. E.g. parameter=value1;value2.
*/
const VALUE_SEPARATOR = ';';

public string $column;
private string $argument;
public array $values;
public string $type;
public string $operator;

public string $cf_field_identificator = 'custom_field_id';
public string $cf_field_value = '';

private ModelConfig $modelConfig;

/**
* @param ModelConfig $modelConfig
* @param OperatorsConfig $operatorsConfig
* @param array $arguments
*
* @throws JsonQueryBuilderException
*/
public function __construct(ModelConfig $modelConfig, OperatorsConfig $operatorsConfig, array $arguments)
{
$this->modelConfig = $modelConfig;

foreach ($arguments as $col => $val) {
if (str_contains($col, $this->cf_field_identificator)) {
$this->cf_field_value = $val;
} else {
$this->column = $col;
$this->argument = $val;
}
}

$this->checkForForbiddenColumns();

$this->operator = $this->parseOperator($operatorsConfig->getOperators(), $this->argument);
$arguments = str_replace($this->operator, '', $this->argument);
$this->values = $this->splitValues($arguments);
$this->type = $this->getColumnType();
}

/**
* @param $operators
* @param string $argument
* @return string
*
* @throws JsonQueryBuilderException
*/
protected function parseOperator($operators, string $argument): string
{
foreach ($operators as $operator) {
$argumentHasOperator = strpos($argument, $operator) !== false;

if (!$argumentHasOperator) {
continue;
}

return $operator;
}

throw new JsonQueryBuilderException("No valid callback registered for $argument. Are you missing an operator?");
}

/**
* Split values by a given separator.
*
* Input: val1;val2
*
* Output: val1
* val2
*
* @param string $values
* @return array
*
* @throws JsonQueryBuilderException
*/
protected function splitValues(string $values): array
{
$valueArray = explode(self::VALUE_SEPARATOR, $values);
$cleanedUpValues = $this->cleanValues($valueArray);

if (count($cleanedUpValues) < 1) {
throw new JsonQueryBuilderException("Column '$this->column' is missing a value.");
}

return $cleanedUpValues;
}

/**
* @return string
*
* @throws JsonQueryBuilderException
*/
protected function getColumnType(): string
{
$columns = $this->modelConfig->getModelColumns();

if (!array_key_exists($this->column, $columns)) {
// TODO: integrate recursive column check for related models?
return 'generic';
}

return $columns[$this->column];
}

/**
* Check if global forbidden key is used.
*
* @throws JsonQueryBuilderException
*/
protected function checkForForbiddenColumns()
{
$forbiddenKeys = Config::get('asseco-json-query-builder.global_forbidden_columns');
$forbiddenKeys = $this->modelConfig->getForbidden($forbiddenKeys);

if (in_array($this->column, $forbiddenKeys)) {
throw new JsonQueryBuilderException("Searching by '$this->column' field is forbidden. Check the configuration if this is not a desirable behavior.");
}
}
}
22 changes: 19 additions & 3 deletions src/RequestParameters/SearchParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
namespace Asseco\JsonQueryBuilder\RequestParameters;

use Asseco\JsonQueryBuilder\Config\OperatorsConfig;
use Asseco\JsonQueryBuilder\CustomFieldSearchParser;
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
use Asseco\JsonQueryBuilder\JsonQuery;
use Asseco\JsonQueryBuilder\SearchCallbacks\AbstractCallback;
use Asseco\JsonQueryBuilder\SearchParser;
use Asseco\JsonQueryBuilder\SearchParserInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;

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

const AND_INCLUSIVE_CF = '&&_INC_CF';

const LARAVEL_WHERE = 'where';
const LARAVEL_OR_WHERE = 'orWhere';

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

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

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

protected function isTopLevelInclusiveCFOperator($key): bool
{
return in_array($key, [self::AND_INCLUSIVE_CF], true);
}

/**
* @param string $boolOperator
* @return string
Expand All @@ -95,7 +111,7 @@ protected function isTopLevelBoolOperator($key): bool
*/
protected function getQueryFunctionName(string $boolOperator): string
{
if ($boolOperator === self::AND) {
if ($boolOperator === self::AND || $boolOperator === self::AND_INCLUSIVE_CF) {
return self::LARAVEL_WHERE;
} elseif ($boolOperator === self::OR) {
return self::LARAVEL_OR_WHERE;
Expand Down Expand Up @@ -186,7 +202,7 @@ protected function splitByBoolOperators($argument): array
*
* @throws JsonQueryBuilderException
*/
protected function appendSingle(Builder $builder, OperatorsConfig $operatorsConfig, SearchParser $searchParser): void
protected function appendSingle(Builder $builder, OperatorsConfig $operatorsConfig, SearchParserInterface $searchParser): void
{
$callbackClassName = $operatorsConfig->getCallbackClassFromOperator($searchParser->operator);

Expand Down
18 changes: 14 additions & 4 deletions src/SearchCallbacks/AbstractCallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
namespace Asseco\JsonQueryBuilder\SearchCallbacks;

use Asseco\JsonQueryBuilder\CategorizedValues;
use Asseco\JsonQueryBuilder\CustomFieldSearchParser;
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
use Asseco\JsonQueryBuilder\SearchParser;
use Asseco\JsonQueryBuilder\SearchParserInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
Expand All @@ -15,7 +16,7 @@
abstract class AbstractCallback
{
protected Builder $builder;
protected SearchParser $searchParser;
protected SearchParserInterface $searchParser;
protected CategorizedValues $categorizedValues;

protected const DATE_FIELDS = [
Expand All @@ -26,11 +27,11 @@ abstract class AbstractCallback
* AbstractCallback constructor.
*
* @param Builder $builder
* @param SearchParser $searchParser
* @param SearchParserInterface $searchParser
*
* @throws JsonQueryBuilderException
*/
public function __construct(Builder $builder, SearchParser $searchParser)
public function __construct(Builder $builder, SearchParserInterface $searchParser)
{
$this->builder = $builder;
$this->searchParser = $searchParser;
Expand All @@ -50,6 +51,7 @@ function (Builder $builder) {
},
function (Builder $builder) {
$this->execute($builder, $this->searchParser->column, $this->categorizedValues);
$this->checkExecuteForCustomfieldsParameter($builder);
}
);
}
Expand Down Expand Up @@ -87,6 +89,7 @@ protected function appendRelations(Builder $builder, string $column, Categorized
}

$this->execute($builder, $relatedColumns, $values);
$this->checkExecuteForCustomfieldsParameter($builder);
});
}

Expand Down Expand Up @@ -164,4 +167,11 @@ protected function getLikeOperator(): string

return 'LIKE';
}

protected function checkExecuteForCustomfieldsParameter($builder)
{
if ($this->searchParser instanceof CustomFieldSearchParser) {
$builder->where($this->searchParser->cf_field_identificator, '=', $this->searchParser->cf_field_value);
}
}
}
2 changes: 1 addition & 1 deletion src/SearchParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Asseco\JsonQueryBuilder\Traits\CleansValues;
use Illuminate\Support\Facades\Config;

class SearchParser
class SearchParser implements SearchParserInterface
{
use CleansValues;

Expand Down
9 changes: 9 additions & 0 deletions src/SearchParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Asseco\JsonQueryBuilder;

interface SearchParserInterface
{
}
Loading

0 comments on commit 58151c7

Please sign in to comment.