Skip to content

Commit

Permalink
add remaining filter+comments
Browse files Browse the repository at this point in the history
  • Loading branch information
recursivetree committed Nov 3, 2023
1 parent be91581 commit 8da7326
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 21 deletions.
8 changes: 8 additions & 0 deletions src/Exceptions/InvalidFilterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Seat\Web\Exceptions;

class InvalidFilterException extends \Exception
{

}
66 changes: 45 additions & 21 deletions src/Models/Filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Seat\Web\Exceptions\InvalidFilterException;
use stdClass;

/**
Expand All @@ -34,65 +35,88 @@
trait Filterable
{
/**
* The filters to use.
* @return \stdClass
*/
abstract public function getFilters(): stdClass;

/**
* @param \Illuminate\Database\Eloquent\Model $member
* @return bool
* Check if an entity is eligible
* @param Model $member The entity to check
* @return bool Whether the entity is eligible
* @throws InvalidFilterException If a invalid filter configuration is used
*/
final public function isEligible(Model $member): bool
{
// in case no filters exists, bypass check and return not eligible
if (!property_exists($this->getFilters(), 'and') && !property_exists($this->getFilters(), 'or'))
return false;
throw new InvalidFilterException('root filter configuration is not a rule group');

$query = $member->newQuery();
$query = new QueryGroupBuilder($member->newQuery(), true);

// make sure we only allow results of the entity we are checking
$query->where($member->getKeyName(),$member->getKey());
// wrap this in an inner query to ensure we have the correct entity and the filter applies
// make sure we only allow results of the entity we are checking count
$query->where(function (Builder $inner_query) use ($member) {
$inner_query->where($member->getKeyName(),$member->getKey());
});

// wrap this in an inner query to ensure it is '(correct_entity_check) AND (rule1 AND/OR rule2)'
$query->where(function ($inner_query){
$this->applyGroup($inner_query, $this->getFilters());
});

//return dd($query->toRawSql(), $query->exists());
return $query->exists();
return $query->getUnderlyingQuery()->exists();
}

private function applyGroup(Builder $query, object $group): void
/**
* Applies a filter group to $query
* @param Builder $query the query to add the filter group to
* @param stdClass $group the filter group configuration
* @throws InvalidFilterException
*/
private function applyGroup(Builder $query, stdClass $group): void
{
$query_group = new QueryGroupBuilder($query, property_exists($group, 'and'));

$rules = $query_group->isAndGroup() ? $group->and : $group->or;

foreach ($rules as $rule){
// check if this is a nested group or not
if(property_exists($rule,'path')){
$this->applyRule($query_group, $rule);
} else {
// this is a nested group
$query->where(function ($group_query) use ($group) {
$this->applyGroup($group_query, $group);
$query->where(function ($group_query) use ($rule) {
$this->applyGroup($group_query, $rule);
});
}
}
}

private function applyRule(QueryGroupBuilder $query, object $rule): void {
/**
* Applies a rule to a query group
* @param QueryGroupBuilder $query the query to add the rule to
* @param stdClass $rule the rule configuration
* @throws InvalidFilterException
*/
private function applyRule(QueryGroupBuilder $query, stdClass $rule): void {
// 'is' operator
if($rule->operator === '='){
$query->whereHas($rule->path, function ($inner_query) use ($rule) {
if($rule->operator === '=' || $rule->operator === '<' || $rule->operator==='>'){
// normal comparison operations need to relation to exist
$query->whereHas($rule->path, function (Builder $inner_query) use ($rule) {
$inner_query->where($rule->field,$rule->operator, $rule->criteria);
});
} else if ($rule->operator === '<>') {
$query->where(function (Builder $inner_query) use ($rule) {
$inner_query->whereDoesntHave($rule->path, function ($final_query) use ($rule) {
$final_query->where($rule->field, $rule->criteria);
});
} else if ($rule->operator === '<>' || $rule->operator === '!=') {
// not equal is special cased since a missing relation is the same as not equal
$query->whereDoesntHave($rule->path, function (Builder $inner_query) use ($rule) {
$inner_query->where($rule->field, $rule->criteria);
});
} else if($rule->operator === 'contains'){
// contains is maybe a misleading name, since it actually checks if json contains a value
$query->whereHas($rule->path, function (Builder $inner_query) use ($rule) {
$inner_query->whereJsonContains($rule->field,$rule->criteria);
});
} else {
throw new \Exception('not implemented');
throw new InvalidFilterException(sprintf('Unknown rule operator: \'%s\'',$rule->operator));
}
}
}
57 changes: 57 additions & 0 deletions src/Models/QueryGroupBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,52 @@
use Closure;
use Illuminate\Database\Eloquent\Builder;

/**
* Helper to build query where clauses that are either connected by AND or by OR
*/
class QueryGroupBuilder
{
/**
* @var bool Whether the where clauses should be linked by AND
*/
protected bool $is_and_group;

/**
* @var Builder The query builder to add the where clauses to
*/
protected Builder $query;

/**
* @param Builder $query The query builder to add the where clauses to
* @param bool $is_and_group Whether the where clauses should be linked by AND
*/
public function __construct(Builder $query, bool $is_and_group){
$this->query = $query;
$this->is_and_group = $is_and_group;
}

/**
* @return bool Returns true when the where clauses are linked by AND
*/
public function isAndGroup(): bool {
return $this->is_and_group;
}

/**
* @return Builder The underlying query builder used for this group
*/
public function getUnderlyingQuery(): Builder
{
return $this->query;
}

/**
* Either adds a 'where' or 'orWhere' to the query, depending on if it is an AND linked group or not
* @param Closure $callback a callback to add constraints
* @return $this
* @see Builder::where
* @see Builder::orWhere
*/
public function where(Closure $callback): QueryGroupBuilder {
if($this->is_and_group){
$this->query->where($callback);
Expand All @@ -27,6 +59,14 @@ public function where(Closure $callback): QueryGroupBuilder {
return $this;
}

/**
* Either adds a 'whereHas' or 'orWhereHas' to the query, depending on if it is an AND linked group or not
* @param string $relation the relation to check for existence
* @param Closure $callback a callback to add more constraints
* @return $this
* @see Builder::whereHas
* @see Builder::orWhereHas
*/
public function whereHas(string $relation, Closure $callback): QueryGroupBuilder {
if($this->is_and_group){
$this->query->whereHas($relation, $callback);
Expand All @@ -35,4 +75,21 @@ public function whereHas(string $relation, Closure $callback): QueryGroupBuilder
}
return $this;
}

/**
* Either adds a 'whereDoesntHave' or 'orWhereDoesntHave' to the query, depending on if it is an AND linked group or not
* @param string $relation the relation to check for absence
* @param Closure $callback a callback to add more constraints
* @return $this
* @see Builder::whereDoesntHave
* @see Builder::orWhereDoesntHave
*/
public function whereDoesntHave(string $relation, Closure $callback): QueryGroupBuilder {
if($this->is_and_group){
$this->query->whereDoesntHave($relation, $callback);
} else {
$this->query->orWhereDoesntHave($relation, $callback);
}
return $this;
}
}

0 comments on commit 8da7326

Please sign in to comment.