diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
commit | 4ce65d59ca91871cfd126497158200a818720bce (patch) | |
tree | e277def01fc7eba7dbc21c4a4ae5576e8aa2cf1f /vendor/ipl/orm/src/Compat | |
parent | Initial commit. (diff) | |
download | icinga-php-library-96ecad332d1684426665842bfcebf1aa4692a020.tar.xz icinga-php-library-96ecad332d1684426665842bfcebf1aa4692a020.zip |
Adding upstream version 0.13.1.upstream/0.13.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ipl/orm/src/Compat')
-rw-r--r-- | vendor/ipl/orm/src/Compat/FilterProcessor.php | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/vendor/ipl/orm/src/Compat/FilterProcessor.php b/vendor/ipl/orm/src/Compat/FilterProcessor.php new file mode 100644 index 0000000..7956898 --- /dev/null +++ b/vendor/ipl/orm/src/Compat/FilterProcessor.php @@ -0,0 +1,375 @@ +<?php + +namespace ipl\Orm\Compat; + +use AppendIterator; +use ArrayIterator; +use ipl\Orm\Exception\InvalidColumnException; +use ipl\Orm\Exception\ValueConversionException; +use ipl\Orm\Query; +use ipl\Orm\Relation; +use ipl\Orm\UnionQuery; +use ipl\Sql\Filter\Exists; +use ipl\Sql\Filter\In; +use ipl\Sql\Filter\NotExists; +use ipl\Sql\Filter\NotIn; +use ipl\Stdlib\Contract\Filterable; +use ipl\Stdlib\Filter\MetaDataProvider; +use ipl\Stdlib\Filter; + +class FilterProcessor extends \ipl\Sql\Compat\FilterProcessor +{ + protected $baseJoins = []; + + protected $madeJoins = []; + + /** + * Require and resolve the filter rule and apply it on the query + * + * Note that this applies the filter to {@see Query::$selectBase} + * directly and bypasses {@see Query::$filter}. If this is not + * desired, utilize the {@see Filterable} functions of the query. + * + * @param Filter\Rule $filter + * @param Query $query + */ + public static function apply(Filter\Rule $filter, Query $query) + { + if ($query instanceof UnionQuery) { + foreach ($query->getUnions() as $union) { + static::apply($filter, $union); + } + + return; + } + + if (! $filter instanceof Filter\Chain || ! $filter->isEmpty()) { + $filter = clone $filter; + if (! $filter instanceof Filter\Chain) { + $filter = Filter::all($filter); + } + + static::resolveFilter($filter, $query); + + $where = static::assembleFilter($filter); + + if ($where) { + $operator = array_shift($where); + $conditions = array_shift($where); + + $query->getSelectBase()->where($conditions, $operator); + } + } + } + + /** + * Resolve the filter in order to apply it on the query + * + * @param Filter\Chain $filter + * @param Query $query + * + * @return void + */ + public static function resolveFilter(Filter\Chain $filter, Query $query) + { + $processor = new static(); + foreach ($query->getUtilize() as $path => $_) { + $processor->baseJoins[$path] = true; + } + + $processor->requireAndResolveFilterColumns($filter, $query); + } + + protected function requireAndResolveFilterColumns(Filter\Rule $filter, Query $query, $forceOptimization = null) + { + if ($filter instanceof Filter\Condition) { + if ( + $filter instanceof Exists + || $filter instanceof NotExists + || $filter instanceof In + || $filter instanceof NotIn + ) { + return; + } + + $resolver = $query->getResolver(); + $baseTable = $query->getModel()->getTableAlias(); + $column = $resolver->qualifyPath( + $filter->metaData()->get('columnName', $filter->getColumn()), + $baseTable + ); + + $filter->metaData()->set('columnPath', $column); + + list($relationPath, $columnName) = preg_split('/\.(?=[^.]+$)/', $column); + + $subject = null; + $relations = new AppendIterator(); + $relations->append(new ArrayIterator([$baseTable => null])); + $relations->append($resolver->resolveRelations($relationPath)); + $behaviorsApplied = $filter->metaData()->get('behaviorsApplied', false); + foreach ($relations as $path => $relation) { + $columnName = substr($column, strlen($path) + 1); + + if ($path === $baseTable) { + $subject = $query->getModel(); + } else { + /** @var Relation $relation */ + $subject = $relation->getTarget(); + } + + $subjectBehaviors = $resolver->getBehaviors($subject); + // This is only used within the Binary behavior in rewriteCondition(). + $filter->metaData()->set('originalValue', $filter->getValue()); + + if (! $behaviorsApplied) { + try { + // Prepare filter as if it were final to allow full control for rewrite filter behaviors + $filter->setValue($subjectBehaviors->persistProperty($filter->getValue(), $columnName)); + } catch (ValueConversionException $_) { + // The search bar may submit values with wildcards or whatever the user has entered. + // In this case, we can simply ignore this error instead of rendering a stack trace. + } + } + + $filter->setColumn($resolver->getAlias($subject) . '.' . $columnName); + $filter->metaData()->set('columnName', $columnName); + $filter->metaData()->set('relationPath', $path); + + if (! $behaviorsApplied) { + $rewrittenFilter = $subjectBehaviors->rewriteCondition($filter, $path . '.'); + if ($rewrittenFilter !== null) { + return $this->requireAndResolveFilterColumns($rewrittenFilter, $query, $forceOptimization) + ?: $rewrittenFilter; + } + } + } + + /** + * We have applied all the subject behaviors for this filter condition, so set this metadata to prevent + * the behaviors from being applied for the same filter condition over again later in case of a subquery. + * The behaviors are processed again due to $subQueryFilter being evaluated by this processor as part of + * the subquery. The reason for this is the application of aliases used in said subquery. Since this is + * part of the filter column qualification, and the behaviors are not, this should be separately done. + * There's a similar comment in {@see Query::createSubQuery()} which should be considered when working + * on improving this. + */ + $filter->metaData()->set('behaviorsApplied', true); + + if (! $resolver->hasSelectableColumn($subject, $columnName)) { + throw new InvalidColumnException($columnName, $subject); + } + + if ($relationPath !== $baseTable) { + $query->utilize($relationPath); + $this->madeJoins[$relationPath][] = $filter; + } + } else { + /** @var Filter\Chain $filter */ + + if ($filter->metaData()->has('forceOptimization')) { + // Rules can override the default behavior how it's determined that they need to be + // optimized. If it's done by a chain, it applies to all of its children. + $forceOptimization = $filter->metaData()->get('forceOptimization'); + } + + $subQueryGroups = []; + /** @var Filter\Rule[] $outsourcedRules */ + $outsourcedRules = []; + foreach ($filter as $child) { + /** @var Filter\Rule $child */ + $rewrittenFilter = $this->requireAndResolveFilterColumns($child, $query, $forceOptimization); + if ($rewrittenFilter !== null) { + $filter->replace($child, $rewrittenFilter); + $child = $rewrittenFilter; + } + + $optimizeChild = $forceOptimization; + if ($child instanceof MetaDataProvider && $child->metaData()->has('forceOptimization')) { + $optimizeChild = $child->metaData()->get('forceOptimization'); + } + + // We only optimize rules in a single level, nested chains are ignored + if ($child instanceof Filter\Condition && $child->metaData()->has('relationPath')) { + $relationPath = $child->metaData()->get('relationPath'); + if ( + $relationPath !== $query->getModel()->getTableAlias() // Not the base table + && ( + $optimizeChild !== null && $optimizeChild + || ( + $optimizeChild === null + && ! isset($query->getWith()[$relationPath]) // Not a selected join + && ! $query->getResolver()->isDistinctRelation($relationPath) // Not a to-one relation + ) + ) + ) { + $subQueryGroups[$relationPath][$child->getColumn()][get_class($child)][] = $child; + + // Register all rules that are going to be put into sub queries, for later cleanup + $outsourcedRules[] = $child; + } + } + } + + foreach ($subQueryGroups as $relationPath => $columns) { + $generalRules = []; + foreach ($columns as $column => $comparisons) { + if (isset($comparisons[Filter\Unequal::class]) || isset($comparisons[Filter\Unlike::class])) { + // If there's a unequal (!=) comparison for any column, all other comparisons (for the same + // column) also need to be outsourced to their own sub query. Regardless of their amount of + // occurrence. This is because `$generalRules` apply to all comparisons of such a column and + // need to be applied to all sub queries. + continue; + } + + // Single occurring columns don't need their own sub query + foreach ($comparisons as $conditionClass => $rules) { + if (count($rules) === 1) { + $generalRules[] = $rules[0]; + unset($columns[$column][$conditionClass]); + } + } + + if (empty($columns[$column])) { + unset($columns[$column]); + } + } + + $count = null; + $baseFilters = null; + $subQueryFilters = []; + foreach ($columns as $column => $comparisons) { + foreach ($comparisons as $conditionClass => $rules) { + if ($conditionClass === Filter\Unequal::class || $conditionClass === Filter\Unlike::class) { + // Unequal comparisons are always put into their own sub query + $subQueryFilters[] = [$rules, count($rules), true]; + } elseif (count($rules) > $count) { + // If there are multiple columns used multiple times in the same relation, we have to decide + // which to use as the primary comparison. That is the column that is used most often. + if (! empty($baseFilters)) { + array_push($generalRules, ...$baseFilters); + } + + $count = count($rules); + $baseFilters = $rules; + } else { + array_push($generalRules, ...$rules); + } + } + } + + if (! empty($baseFilters) || ! empty($generalRules)) { + $subQueryFilters[] = [$baseFilters ?: $generalRules, $count, false]; + } + + foreach ($subQueryFilters as list($filters, $count, $negate)) { + $subQueryFilter = null; + if ($count !== null) { + $aggregateFilter = Filter::any(); + foreach ($filters as $condition) { + if ($negate) { + if ($condition instanceof Filter\Unequal) { + $negation = Filter::equal($condition->getColumn(), $condition->getValue()); + } else { // if ($condition instanceof Filter\Unlike) + $negation = Filter::like($condition->getColumn(), $condition->getValue()); + } + + $negation->metaData()->merge($condition->metaData()); + $condition = $negation; + $count = 1; + } + + switch (true) { + case $filter instanceof Filter\All: + $aggregateFilter->add(Filter::all($condition, ...$generalRules)); + break; + case $filter instanceof Filter\Any: + $aggregateFilter->add(Filter::any($condition, ...$generalRules)); + break; + case $filter instanceof Filter\None: + $aggregateFilter->add(Filter::none($condition, ...$generalRules)); + break; + } + } + + $subQueryFilter = $aggregateFilter; + } else { + switch (true) { + case $filter instanceof Filter\All: + $subQueryFilter = Filter::all(...$filters); + break; + case $filter instanceof Filter\Any: + $subQueryFilter = Filter::any(...$filters); + break; + case $filter instanceof Filter\None: + $subQueryFilter = Filter::none(...$filters); + break; + } + } + + $relation = $query->getResolver()->resolveRelation($relationPath); + $subQuery = $query->createSubQuery($relation->getTarget(), $relationPath, null, false); + $subQuery->getResolver()->setAliasPrefix('sub_'); + + $subQuery->filter($subQueryFilter); + + $subQuerySelect = $subQuery->assembleSelect()->resetOrderBy(); + + if ($count !== null && ($negate || $filter instanceof Filter\All)) { + $targetKeys = join( + ',', + array_values( + $subQuery->getResolver()->qualifyColumns( + (array) $subQuery->getModel()->getKeyName(), + $subQuery->getModel() + ) + ) + ); + + $subQuerySelect->having(["COUNT(DISTINCT $targetKeys) >= ?" => $count]); + $subQuerySelect->groupBy(array_values($subQuerySelect->getColumns())); + } + + // TODO: Qualification is only necessary since the `In` and `NotIn` conditions are ignored by + // requireAndResolveFilterColumns(). In case it supports not only single columns but also + // multiple, this might be reduced to: $keyTuple = (array) $query->getModel()->getKeyName() + $keyTuple = array_values( + $query->getResolver()->qualifyColumns( + (array) $query->getModel()->getKeyName(), + $query->getModel() + ) + ); + + if ($negate) { + $filter->add(new NotIn($keyTuple, $subQuerySelect)); + } else { + $filter->add(new In($keyTuple, $subQuerySelect)); + } + } + } + + foreach ($outsourcedRules as $rule) { + // Remove joins solely used for filter conditions + foreach ($this->madeJoins as $joinPath => & $madeBy) { + $madeBy = array_filter( + $madeBy, + function ($relationFilter) use ($rule) { + return $rule !== $relationFilter + && (! $rule instanceof Filter\Chain || ! $rule->has($relationFilter)); + } + ); + + if (empty($madeBy)) { + if (! isset($this->baseJoins[$joinPath])) { + $query->omit($joinPath); + } + + unset($this->madeJoins[$joinPath]); + } + } + + $filter->remove($rule); + } + } + } +} |