Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend for cf custom field search parser #50

Merged
merged 3 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading