mirror of
https://github.com/Icinga/icingadb-web.git
synced 2026-05-28 04:36:06 -04:00
Introduce QueryValuesProvider
Decouple value suggestion logic into a standalone `Generator` instance. This allows callers to fetch column values without any `Suggestions` context. A plain `foreach` on the provider is enough. `ObjectSuggestions::fetchValueSuggestions` is reduced to a thin wrapper that resolves label-to-path input before delegating to the new class.
This commit is contained in:
parent
8736be428f
commit
9d1dfd92d3
2 changed files with 123 additions and 66 deletions
115
library/Icingadb/Data/QueryValuesProvider.php
Normal file
115
library/Icingadb/Data/QueryValuesProvider.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
namespace Icinga\Module\Icingadb\Data;
|
||||
|
||||
use Icinga\Module\Icingadb\Common\Auth;
|
||||
use Icinga\Module\Icingadb\Util\ObjectSuggestionsCursor;
|
||||
use ipl\I18n\Translation;
|
||||
use ipl\Orm\Exception\InvalidColumnException;
|
||||
use ipl\Orm\Exception\InvalidRelationException;
|
||||
use ipl\Orm\Query;
|
||||
use ipl\Stdlib\BaseFilter;
|
||||
use ipl\Stdlib\Filter;
|
||||
use ipl\Web\Control\SearchBar\SearchException;
|
||||
use IteratorAggregate;
|
||||
use PDO;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Provide value suggestions for a given column and query
|
||||
*/
|
||||
class QueryValuesProvider implements IteratorAggregate
|
||||
{
|
||||
use Auth;
|
||||
use BaseFilter;
|
||||
use Translation;
|
||||
|
||||
/** @var Query The query to collect values from */
|
||||
protected Query $query;
|
||||
|
||||
/** @var string The column to suggest values for */
|
||||
protected string $column;
|
||||
|
||||
/** @var string The search term to suggest values for */
|
||||
protected string $searchTerm;
|
||||
|
||||
/** @var Filter\Chain|Filter\Rule The search filter to apply */
|
||||
protected Filter\Chain|Filter\Rule $searchFilter;
|
||||
|
||||
/**
|
||||
* Create a new QueryValuesProvider
|
||||
*
|
||||
* @param Query $query The query to collect values from
|
||||
* @param string $column The column to suggest values for
|
||||
* @param string $searchTerm The search term to suggest values for
|
||||
* @param Filter\Chain $searchFilter The search filter to apply
|
||||
*/
|
||||
public function __construct(Query $query, string $column, string $searchTerm, Filter\Chain $searchFilter)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->column = $column;
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->searchFilter = $searchFilter;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
$columnPath = $this->query->getResolver()->qualifyPath($this->column, $this->query->getModel()->getTableName());
|
||||
[$targetPath, $columnName] = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath, 2);
|
||||
|
||||
$isCustomVar = false;
|
||||
if (str_ends_with($targetPath, '.vars')) {
|
||||
$isCustomVar = true;
|
||||
$targetPath = substr($targetPath, 0, -4) . 'customvar_flat';
|
||||
}
|
||||
|
||||
if (str_contains($targetPath, '.')) {
|
||||
try {
|
||||
$this->query->with($targetPath); // TODO: Remove this, once ipl/orm does it as early
|
||||
} catch (InvalidRelationException $e) {
|
||||
throw new SearchException(sprintf($this->translate('"%s" is not a valid relation'), $e->getRelation()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCustomVar) {
|
||||
$columnPath = $targetPath . '.flatvalue';
|
||||
$this->query->filter(Filter::like($targetPath . '.flatname', $columnName));
|
||||
}
|
||||
|
||||
$inputFilter = Filter::like($columnPath, $this->searchTerm);
|
||||
$this->query->columns($columnPath);
|
||||
$this->query->orderBy($columnPath);
|
||||
|
||||
if ($this->searchFilter instanceof Filter\None) {
|
||||
$this->query->filter($inputFilter);
|
||||
} elseif ($this->searchFilter instanceof Filter\All) {
|
||||
$this->searchFilter->add($inputFilter);
|
||||
|
||||
// There may be columns part of $searchFilter which target the base table. These must be
|
||||
// optimized, otherwise they influence what we'll suggest to the user. (i.e. less)
|
||||
// The $inputFilter on the other hand must not be optimized, which it wouldn't, but since
|
||||
// we force optimization on its parent chain, we have to negate that.
|
||||
$this->searchFilter->metaData()->set('forceOptimization', true);
|
||||
$inputFilter->metaData()->set('forceOptimization', false);
|
||||
} else {
|
||||
$this->searchFilter = $inputFilter;
|
||||
}
|
||||
|
||||
$this->query->filter($this->searchFilter);
|
||||
|
||||
$this->applyRestrictions($this->query);
|
||||
if ($this->hasBaseFilter()) {
|
||||
$this->query->filter($this->getBaseFilter());
|
||||
}
|
||||
|
||||
try {
|
||||
return (new ObjectSuggestionsCursor($this->query->getDb(), $this->query->assembleSelect()->distinct()))
|
||||
->setFetchMode(PDO::FETCH_COLUMN);
|
||||
} catch (InvalidColumnException $e) {
|
||||
throw new SearchException(sprintf($this->translate('"%s" is not a valid column'), $e->getColumn()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,18 +8,13 @@ namespace Icinga\Module\Icingadb\Web\Control\SearchBar;
|
|||
use Icinga\Module\Icingadb\Common\Auth;
|
||||
use Icinga\Module\Icingadb\Common\Database;
|
||||
use Icinga\Module\Icingadb\Data\QueryColumnsProvider;
|
||||
use Icinga\Module\Icingadb\Util\ObjectSuggestionsCursor;
|
||||
use Icinga\Module\Icingadb\Data\QueryValuesProvider;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Orm\Exception\InvalidColumnException;
|
||||
use ipl\Orm\Exception\InvalidRelationException;
|
||||
use ipl\Orm\Model;
|
||||
use ipl\Orm\Query;
|
||||
use ipl\Stdlib\BaseFilter;
|
||||
use ipl\Stdlib\Filter;
|
||||
use ipl\Stdlib\Seq;
|
||||
use ipl\Web\Control\SearchBar\SearchException;
|
||||
use ipl\Web\Control\SearchBar\Suggestions;
|
||||
use PDO;
|
||||
|
||||
class ObjectSuggestions extends Suggestions
|
||||
{
|
||||
|
|
@ -92,15 +87,6 @@ class ObjectSuggestions extends Suggestions
|
|||
return QueryColumnsProvider::shouldShowRelationFor($column, $this->getModel());
|
||||
}
|
||||
|
||||
private function applyBaseFilter(Query $query): void
|
||||
{
|
||||
$this->applyRestrictions($query);
|
||||
|
||||
if ($this->hasBaseFilter()) {
|
||||
$query->filter($this->getBaseFilter());
|
||||
}
|
||||
}
|
||||
|
||||
protected function createQuickSearchFilter($searchTerm)
|
||||
{
|
||||
$model = $this->getModel();
|
||||
|
|
@ -126,11 +112,11 @@ class ObjectSuggestions extends Suggestions
|
|||
$query = $model::on($this->getDb());
|
||||
$query->limit(static::DEFAULT_LIMIT);
|
||||
|
||||
if (strpos($column, ' ') !== false) {
|
||||
if (str_contains($column, ' ')) {
|
||||
// $column may be a label
|
||||
list($path, $_) = Seq::find(
|
||||
[$path, $_] = Seq::find(
|
||||
$this->fixedColumns
|
||||
?? QueryColumnsProvider::collectFilterColumns($query->getModel(), $query->getResolver()),
|
||||
?? QueryColumnsProvider::collectFilterColumns($query->getModel(), $query->getResolver()),
|
||||
$column,
|
||||
false
|
||||
);
|
||||
|
|
@ -139,57 +125,13 @@ class ObjectSuggestions extends Suggestions
|
|||
}
|
||||
}
|
||||
|
||||
$columnPath = $query->getResolver()->qualifyPath($column, $model->getTableName());
|
||||
list($targetPath, $columnName) = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath, 2);
|
||||
$provider = new QueryValuesProvider($query, $column, $searchTerm, $searchFilter);
|
||||
|
||||
$isCustomVar = false;
|
||||
if (substr($targetPath, -5) === '.vars') {
|
||||
$isCustomVar = true;
|
||||
$targetPath = substr($targetPath, 0, -4) . 'customvar_flat';
|
||||
if ($this->hasBaseFilter()) {
|
||||
$provider->setBaseFilter($this->getBaseFilter());
|
||||
}
|
||||
|
||||
if (strpos($targetPath, '.') !== false) {
|
||||
try {
|
||||
$query->with($targetPath); // TODO: Remove this, once ipl/orm does it as early
|
||||
} catch (InvalidRelationException $e) {
|
||||
throw new SearchException(sprintf(t('"%s" is not a valid relation'), $e->getRelation()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCustomVar) {
|
||||
$columnPath = $targetPath . '.flatvalue';
|
||||
$query->filter(Filter::like($targetPath . '.flatname', $columnName));
|
||||
}
|
||||
|
||||
$inputFilter = Filter::like($columnPath, $searchTerm);
|
||||
$query->columns($columnPath);
|
||||
$query->orderBy($columnPath);
|
||||
|
||||
// This had so many iterations, if it still doesn't work, consider removing it entirely :(
|
||||
if ($searchFilter instanceof Filter\None) {
|
||||
$query->filter($inputFilter);
|
||||
} elseif ($searchFilter instanceof Filter\All) {
|
||||
$searchFilter->add($inputFilter);
|
||||
|
||||
// There may be columns part of $searchFilter which target the base table. These must be
|
||||
// optimized, otherwise they influence what we'll suggest to the user. (i.e. less)
|
||||
// The $inputFilter on the other hand must not be optimized, which it wouldn't, but since
|
||||
// we force optimization on its parent chain, we have to negate that.
|
||||
$searchFilter->metaData()->set('forceOptimization', true);
|
||||
$inputFilter->metaData()->set('forceOptimization', false);
|
||||
} else {
|
||||
$searchFilter = $inputFilter;
|
||||
}
|
||||
|
||||
$query->filter($searchFilter);
|
||||
$this->applyBaseFilter($query);
|
||||
|
||||
try {
|
||||
return (new ObjectSuggestionsCursor($query->getDb(), $query->assembleSelect()->distinct()))
|
||||
->setFetchMode(PDO::FETCH_COLUMN);
|
||||
} catch (InvalidColumnException $e) {
|
||||
throw new SearchException(sprintf(t('"%s" is not a valid column'), $e->getColumn()));
|
||||
}
|
||||
return $provider;
|
||||
}
|
||||
|
||||
protected function fetchColumnSuggestions($searchTerm)
|
||||
|
|
|
|||
Loading…
Reference in a new issue