Skip to content

Commit

Permalink
Revert "Rewrite Filterable trait used for Squad filters" (#663)
Browse files Browse the repository at this point in the history
  • Loading branch information
warlof authored Jan 14, 2024
1 parent fbb4e79 commit 7d10130
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 271 deletions.
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@
.env.php
.env
.idea/

# testing
vendor/
composer.lock
.phpunit.cache/test-results
28 changes: 0 additions & 28 deletions src/Exceptions/InvalidFilterException.php

This file was deleted.

187 changes: 120 additions & 67 deletions src/Models/Filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

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

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

/**
* 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
* @param \Illuminate\Database\Eloquent\Model $member
* @return bool
*/
final public function isEligible(Model $member): bool
{
// in case no filters exists, everyone should be allowed
// this is the case with manual squads
// in case no filters exists, bypass check
if (! property_exists($this->getFilters(), 'and') && ! property_exists($this->getFilters(), 'or'))
return true;

$query = new QueryGroupBuilder($member->newQuery(), true);
// init a new object based on parameter
$class = get_class($member);

// 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());
});
return (new $class)::where($member->getKeyName(), $member->id)
->where(function ($query) {

// 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());
});
// verb will determine what kind of method we have to use (simple andWhere or orWhere)
$verb = property_exists($this->getFilters(), 'and') ? 'whereHas' : 'orWhereHas';

// rules will determine all objects and ruleset in the current object root
$rules = property_exists($this->getFilters(), 'and') ? $this->getFilters()->and : $this->getFilters()->or;

// sort rules by path
$sorted_rules = $this->sortFiltersByRelations($rules);

// TODO: find a way to handle this using recursive loop and determine common patterns
$sorted_rules->each(function ($rules_group, $path) use ($query, $verb) {

if (is_int($path)) {

$parent_verb = $verb == 'whereHas' ? 'where' : 'orWhere';

$query->$parent_verb(function ($q2) use ($rules_group, $parent_verb) {

// all pairs will be group in distinct collection due to previous group by
// as a result, we have to iterate over each members
$rules_group->each(function ($rules) use ($parent_verb, $q2) {

// determine the match kind for the current pair
// sort all rules from this pair in order to ensure relationship consistency
$group_verb = property_exists($rules, 'and') ? 'whereHas' : 'orWhereHas';
$rules_group = $this->sortFiltersByRelations(property_exists($rules, 'and') ?
$rules->and : $rules->or);

$rules_group->each(function ($rules, $path) use ($parent_verb, $group_verb, $q2) {

// prepare query from current pair group
$q2->$parent_verb(function ($q3) use ($rules, $path, $group_verb) {
$q3->$group_verb($path, function ($q4) use ($rules, $group_verb) {

// prevent dummy query by encapsulating rules outside relations
$q4->where(function ($q5) use ($rules, $group_verb) {
$this->applyRules($q5, $group_verb, $rules);
});
});
});
});
});
});

} else {

$rules = $rules_group;

// using group by, we've pair all relationships by their top level relation
// $query->whereHas('characters(.*)', function ($sub_query) { ... }
$query->$verb($path, function ($q2) use ($rules, $verb) {

return $query->getUnderlyingQuery()->exists();
// override the complete rule group with a global where.
// doing it so will prevent SQL query like
// (users.id = tokens.user_id OR character_id = ? OR character_id = ?)
// when multiple rules are applied on same path.
$q2->where(function ($q3) use ($rules, $verb) {
$this->applyRules($q3, $verb, $rules);
});
});
}
});
})->exists();
}

/**
* 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
* @param array $rules
* @return \Illuminate\Support\Collection
*/
private function applyGroup(Builder $query, stdClass $group): void
private function sortFiltersByRelations(array $rules)
{
$query_group = new QueryGroupBuilder($query, property_exists($group, 'and'));
return collect($rules)->sortBy('path')->groupBy(function ($rule) {
if (! property_exists($rule, 'path'))
return false;

$rules = $query_group->isAndGroup() ? $group->and : $group->or;
$relation_members = explode('.', $rule->path);

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_group->where(function ($group_query) use ($rule) {
$this->applyGroup($group_query, $rule);
});
}
}
return $relation_members[0];
});
}

/**
* 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
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $group_verb
* @param array|\Illuminate\Support\Collection $rules
* @return \Illuminate\Database\Eloquent\Builder
*/
private function applyRule(QueryGroupBuilder $query, stdClass $rule): void {
// 'is' operator
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);
});
} elseif ($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);
});
} elseif($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 InvalidFilterException(sprintf('Unknown rule operator: \'%s\'', $rule->operator));
}
private function applyRules(Builder $query, string $group_verb, $rules)
{
$query_operator = $group_verb == 'whereHas' ? 'where' : 'orWhere';

if (is_array($rules))
$rules = collect($rules);

$rules->sortBy('path')->groupBy('path')->each(function ($relation_rules, $relation) use ($query, $query_operator) {
if (strpos($relation, '.') !== false) {
$relation = substr($relation, strpos($relation, '.') + 1);

$query->whereHas($relation, function ($q2) use ($query_operator, $relation_rules) {
$q2->where(function ($q3) use ($relation_rules, $query_operator) {
foreach ($relation_rules as $index => $rule) {
if ($rule->operator == 'contains') {
$json_operator = $query_operator == 'where' ? 'whereJsonContains' : 'orWhereJsonContains';
$q3->$json_operator($rule->field, $rule->criteria);
} else {
$q3->$query_operator($rule->field, $rule->operator, $rule->criteria);
}
}
});
});
} else {
$query->where(function ($q2) use ($relation_rules, $query_operator) {
foreach ($relation_rules as $index => $rule) {
if ($rule->operator == 'contains') {
$json_operator = $query_operator == 'where' ? 'whereJsonContains' : 'orWhereJsonContains';
$q2->$json_operator($rule->field, $rule->criteria);
} else {
$q2->$query_operator($rule->field, $rule->operator, $rule->criteria);
}
}
});
}
});

return $query;
}
}
125 changes: 0 additions & 125 deletions src/Models/QueryGroupBuilder.php

This file was deleted.

Loading

0 comments on commit 7d10130

Please sign in to comment.