2023-10-13 21:06:23 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Lucent\Query;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Contracts\Foundation\Application;
|
|
|
|
|
use Illuminate\Database\Query\Builder;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Lucent\Query\Filter\AndFilter;
|
|
|
|
|
use Lucent\Query\Filter\Argument;
|
|
|
|
|
use Lucent\Query\Filter\Filter;
|
|
|
|
|
use Lucent\Query\Filter\OrFilter;
|
|
|
|
|
|
|
|
|
|
final class FilterParser
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
public function __construct(public Application $app)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array $arguments
|
|
|
|
|
* @return array<Argument>
|
|
|
|
|
*/
|
|
|
|
|
private function formatArguments(array $arguments): array
|
|
|
|
|
{
|
|
|
|
|
return collect($arguments)->reduce(function ($c, $v, $k) {
|
|
|
|
|
$c[] = $this->formatArgument($v, $k);
|
|
|
|
|
return $c;
|
|
|
|
|
}, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function formatArgument(mixed $value, string $filter): Argument
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$operator = $this->detectOperator($filter);
|
2023-11-17 20:21:45 +02:00
|
|
|
|
2023-10-13 21:06:23 +03:00
|
|
|
$field = $this->detectField($filter, $operator);
|
|
|
|
|
$formattedValue = match ($operator) {
|
|
|
|
|
"eq" => $this->formatText($value),
|
|
|
|
|
"ne" => $this->formatText($value),
|
|
|
|
|
"eqnum" => $this->formatNumber($value),
|
|
|
|
|
"nenum" => $this->formatNumber($value),
|
|
|
|
|
"object" => $this->formatText($value),
|
|
|
|
|
"in" => $this->formatListString($value),
|
|
|
|
|
"nin" => $this->formatListString($value),
|
|
|
|
|
"innum" => $this->formatListNum($value),
|
|
|
|
|
"ninnum" => $this->formatListNum($value),
|
|
|
|
|
"eqtrue" => true,
|
|
|
|
|
"eqfalse" => false,
|
|
|
|
|
"netrue" => true,
|
|
|
|
|
"nefalse" => false,
|
2023-10-22 16:09:36 +03:00
|
|
|
"regex" => "%" . strtolower($value) . "%",
|
2023-10-13 21:06:23 +03:00
|
|
|
"gt" => is_numeric($value) ? floatval($value) : $value,
|
|
|
|
|
"gte" => is_numeric($value) ? floatval($value) : $value,
|
|
|
|
|
"lt" => is_numeric($value) ? floatval($value) : $value,
|
|
|
|
|
"lte" => is_numeric($value) ? floatval($value) : $value,
|
|
|
|
|
"null" => null,
|
|
|
|
|
"nnull" => null,
|
|
|
|
|
"exists" => true,
|
|
|
|
|
"nexists" => false,
|
|
|
|
|
default => $value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$matchedOperator = Operator::list()[$operator];
|
|
|
|
|
return new Argument(
|
|
|
|
|
field: str_replace(".", "->", $field),
|
|
|
|
|
operator: $matchedOperator->db,
|
|
|
|
|
value: $formattedValue
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function formatText(string $value): string
|
|
|
|
|
{
|
|
|
|
|
return trim($value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function formatNumber(string $value): float
|
|
|
|
|
{
|
|
|
|
|
return floatval($value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function formatListString(mixed $value): array
|
|
|
|
|
{
|
|
|
|
|
if (is_string($value)) {
|
|
|
|
|
$value = explode(",", $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return array_map(fn($v) => $this->formatText($v), $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function formatListNum(mixed $value): array
|
|
|
|
|
{
|
|
|
|
|
if (\is_string($value)) {
|
|
|
|
|
$value = explode(",", $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return \array_map(fn($v) => $this->formatNumber($v), $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function detectOperator(string $filter): string
|
|
|
|
|
{
|
|
|
|
|
$exploded = \explode("_", $filter);
|
|
|
|
|
$candidate = end($exploded);
|
|
|
|
|
$operatorsListNames = collect(Operator::list())->map(fn($o) => $o->name)->toArray();
|
|
|
|
|
|
|
|
|
|
if (\in_array($candidate, $operatorsListNames)) {
|
|
|
|
|
return $candidate;
|
|
|
|
|
}
|
|
|
|
|
return 'eq';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function detectField(string $filter, string $operator): string
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$exploded = explode("_", $filter);
|
|
|
|
|
$candidate = array_pop($exploded);
|
|
|
|
|
if ($candidate === $operator) {
|
|
|
|
|
return implode("_", $exploded);
|
|
|
|
|
}
|
|
|
|
|
return $filter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function formatReferences(array $referenceArguments): Argument
|
|
|
|
|
{
|
|
|
|
|
$subqueries = collect($referenceArguments)->reduce(function ($c, $v, $k) {
|
|
|
|
|
$keyWithoutRef = str_replace("children.", "", $k);
|
|
|
|
|
[$field] = explode(".", $keyWithoutRef);
|
|
|
|
|
$referenceField = str_replace($field . ".", "", $keyWithoutRef);
|
|
|
|
|
$c[$field][$referenceField] = $v;
|
|
|
|
|
return $c;
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-11-17 20:21:45 +02:00
|
|
|
|
2023-10-13 21:06:23 +03:00
|
|
|
$sourceIds = collect($subqueries)->reduce(function ($c, $subquery, $k) {
|
|
|
|
|
|
|
|
|
|
$query = $this->app->make(Query::class);
|
|
|
|
|
$graph = $query->filter($subquery)->run();
|
|
|
|
|
|
|
|
|
|
if (!$graph->hasResults()) {
|
|
|
|
|
return $c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$targetIds = collect($graph->records)->pluck("id");
|
|
|
|
|
$sourceIds = DB::table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
|
|
|
|
|
return array_merge($c, $sourceIds->toArray());
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return new Argument(
|
|
|
|
|
field: "id",
|
|
|
|
|
operator: "in",
|
|
|
|
|
value: $sourceIds
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function separateMainFromReferenceArguments(Filter $arguments): array
|
|
|
|
|
{
|
|
|
|
|
return collect($arguments->toArray())->partition(function ($v, $k) {
|
|
|
|
|
if (!str_starts_with($k, "children.")) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
})->toArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<Argument>
|
|
|
|
|
*/
|
|
|
|
|
private function parseArguments(Filter $arguments): array
|
|
|
|
|
{
|
|
|
|
|
[$normalArguments, $referenceArguments] = $this->separateMainFromReferenceArguments($arguments);
|
|
|
|
|
|
|
|
|
|
$formattedArguments = $this->formatArguments($normalArguments);
|
|
|
|
|
if (!empty($referenceArguments)) {
|
|
|
|
|
$formattedArguments[] = $this->formatReferences($referenceArguments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $formattedArguments;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function parse(Builder $builder, Filter $filter): Builder
|
|
|
|
|
{
|
|
|
|
|
$arguments = $this->parseArguments($filter);
|
|
|
|
|
return match (get_class($filter)) {
|
|
|
|
|
AndFilter::class => $this->parseAnd($builder, $arguments),
|
|
|
|
|
OrFilter::class => $this->parseOr($builder, $arguments),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<Argument> $arguments
|
|
|
|
|
*/
|
|
|
|
|
private function parseAnd(Builder $builder, array $arguments): Builder
|
|
|
|
|
{
|
|
|
|
|
foreach ($arguments as $argument) {
|
2023-10-25 14:19:44 +03:00
|
|
|
if ($argument->operator == "in") {
|
2023-10-13 21:06:23 +03:00
|
|
|
$builder->whereIn($argument->field, $argument->value);
|
|
|
|
|
} else if ($argument->operator == "nin") {
|
|
|
|
|
$builder->whereNotIn($argument->field, $argument->value);
|
2023-10-25 14:19:44 +03:00
|
|
|
} else if ($argument->operator == "exists") {
|
|
|
|
|
$builder->where($argument->field, "!=", "");
|
|
|
|
|
$builder->where($argument->field, "!=", null);
|
|
|
|
|
} elseif ($argument->operator == "filter") {
|
|
|
|
|
$builder->whereJsonContains($argument->field, [$argument->value]);
|
2023-10-13 21:06:23 +03:00
|
|
|
// target result
|
|
|
|
|
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
|
|
|
|
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
|
|
|
|
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
|
|
|
|
} else {
|
|
|
|
|
$builder->where($argument->field, $argument->operator, $argument->value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<Argument> $arguments
|
|
|
|
|
*/
|
|
|
|
|
private function parseOr(Builder $builder, array $arguments): Builder
|
|
|
|
|
{
|
|
|
|
|
$builder->where(function (Builder $orBuilder) use ($arguments) {
|
|
|
|
|
foreach ($arguments as $argument) {
|
2023-10-25 14:19:44 +03:00
|
|
|
if ($argument->operator == "in") {
|
2023-10-13 21:06:23 +03:00
|
|
|
$orBuilder->orWhereIn($argument->field, $argument->value);
|
|
|
|
|
} else if ($argument->operator == "nin") {
|
|
|
|
|
$orBuilder->orWhereNotIn($argument->field, $argument->value);
|
2023-10-25 14:19:44 +03:00
|
|
|
} else if ($argument->operator == "exists") {
|
|
|
|
|
$orBuilder->where($argument->field, "!=", "");
|
|
|
|
|
$orBuilder->where($argument->field, "!=", null);
|
|
|
|
|
} elseif ($argument->operator == "filter") {
|
|
|
|
|
$orBuilder->whereJsonContains($argument->field, [$argument->value]);
|
2023-10-13 21:06:23 +03:00
|
|
|
// target result
|
|
|
|
|
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
|
|
|
|
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
|
|
|
|
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
|
|
|
|
} else {
|
|
|
|
|
$orBuilder->orWhere($argument->field, $argument->operator, $argument->value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return $builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|