794 lines
22 KiB
PHP
794 lines
22 KiB
PHP
<?php
|
|
/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
|
|
|
|
namespace Icinga\Repository;
|
|
|
|
use Iterator;
|
|
use IteratorAggregate;
|
|
use Traversable;
|
|
use Icinga\Application\Benchmark;
|
|
use Icinga\Application\Logger;
|
|
use Icinga\Data\QueryInterface;
|
|
use Icinga\Data\Filter\Filter;
|
|
use Icinga\Data\FilterColumns;
|
|
use Icinga\Data\SortRules;
|
|
use Icinga\Exception\QueryException;
|
|
|
|
/**
|
|
* Query class supposed to mediate between a repository and its datasource's query
|
|
*/
|
|
class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Iterator
|
|
{
|
|
/**
|
|
* The repository being used
|
|
*
|
|
* @var Repository
|
|
*/
|
|
protected $repository;
|
|
|
|
/**
|
|
* The real query being used
|
|
*
|
|
* @var QueryInterface
|
|
*/
|
|
protected $query;
|
|
|
|
/**
|
|
* The current target to be queried
|
|
*
|
|
* @var mixed
|
|
*/
|
|
protected $target;
|
|
|
|
/**
|
|
* The real query's iterator
|
|
*
|
|
* @var Iterator
|
|
*/
|
|
protected $iterator;
|
|
|
|
/**
|
|
* This query's custom aliases
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $customAliases;
|
|
|
|
/**
|
|
* Create a new repository query
|
|
*
|
|
* @param Repository $repository The repository to use
|
|
*/
|
|
public function __construct(Repository $repository)
|
|
{
|
|
$this->repository = $repository;
|
|
}
|
|
|
|
/**
|
|
* Clone all state relevant properties of this query
|
|
*/
|
|
public function __clone()
|
|
{
|
|
if ($this->query !== null) {
|
|
$this->query = clone $this->query;
|
|
}
|
|
if ($this->iterator !== null) {
|
|
$this->iterator = clone $this->iterator;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a string representation of this query
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString()
|
|
{
|
|
return (string) $this->query;
|
|
}
|
|
|
|
/**
|
|
* Return the real query being used
|
|
*
|
|
* @return QueryInterface
|
|
*/
|
|
public function getQuery()
|
|
{
|
|
return $this->query;
|
|
}
|
|
|
|
/**
|
|
* Set where to fetch which columns
|
|
*
|
|
* This notifies the repository about each desired query column.
|
|
*
|
|
* @param mixed $target The target from which to fetch the columns
|
|
* @param array $columns If null or an empty array, all columns will be fetched
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function from($target, array $columns = null)
|
|
{
|
|
$this->query = $this->repository->getDataSource($target)->select();
|
|
$this->query->from($this->repository->requireTable($target, $this));
|
|
$this->query->columns($this->prepareQueryColumns($target, $columns));
|
|
$this->target = $target;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return the columns to fetch
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getColumns()
|
|
{
|
|
return $this->query->getColumns();
|
|
}
|
|
|
|
/**
|
|
* Set which columns to fetch
|
|
*
|
|
* This notifies the repository about each desired query column.
|
|
*
|
|
* @param array $columns If null or an empty array, all columns will be fetched
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function columns(array $columns)
|
|
{
|
|
$this->query->columns($this->prepareQueryColumns($this->target, $columns));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Resolve the given columns supposed to be fetched
|
|
*
|
|
* This notifies the repository about each desired query column.
|
|
*
|
|
* @param mixed $target The target where to look for each column
|
|
* @param array $desiredColumns Pass null or an empty array to require all query columns
|
|
*
|
|
* @return array The desired columns indexed by their respective alias
|
|
*/
|
|
protected function prepareQueryColumns($target, array $desiredColumns = null)
|
|
{
|
|
$this->customAliases = array();
|
|
if (empty($desiredColumns)) {
|
|
$columns = $this->repository->requireAllQueryColumns($target);
|
|
} else {
|
|
$columns = array();
|
|
foreach ($desiredColumns as $customAlias => $columnAlias) {
|
|
$resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
|
|
if ($resolvedColumn !== $columnAlias) {
|
|
if (is_string($customAlias)) {
|
|
$columns[$customAlias] = $resolvedColumn;
|
|
$this->customAliases[$customAlias] = $columnAlias;
|
|
} else {
|
|
$columns[$columnAlias] = $resolvedColumn;
|
|
}
|
|
} elseif (is_string($customAlias)) {
|
|
$columns[$customAlias] = $columnAlias;
|
|
$this->customAliases[$customAlias] = $columnAlias;
|
|
} else {
|
|
$columns[] = $columnAlias;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $columns;
|
|
}
|
|
|
|
/**
|
|
* Return the native column alias for the given custom alias
|
|
*
|
|
* If no custom alias is found with the given name, it is returned unchanged.
|
|
*
|
|
* @param string $customAlias
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getNativeAlias($customAlias)
|
|
{
|
|
if (isset($this->customAliases[$customAlias])) {
|
|
return $this->customAliases[$customAlias];
|
|
}
|
|
|
|
return $customAlias;
|
|
}
|
|
|
|
/**
|
|
* Return this query's available filter columns with their optional label as key
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFilterColumns()
|
|
{
|
|
return $this->repository->getFilterColumns($this->target);
|
|
}
|
|
|
|
/**
|
|
* Return this query's available search columns
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSearchColumns()
|
|
{
|
|
return $this->repository->getSearchColumns($this->target);
|
|
}
|
|
|
|
/**
|
|
* Filter this query using the given column and value
|
|
*
|
|
* This notifies the repository about the required filter column.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $value
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function where($column, $value = null)
|
|
{
|
|
$this->addFilter(Filter::where($column, $value));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add an additional filter expression to this query
|
|
*
|
|
* This notifies the repository about each required filter column.
|
|
*
|
|
* @param Filter $filter
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function applyFilter(Filter $filter)
|
|
{
|
|
return $this->addFilter($filter);
|
|
}
|
|
|
|
/**
|
|
* Set a filter for this query
|
|
*
|
|
* This notifies the repository about each required filter column.
|
|
*
|
|
* @param Filter $filter
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setFilter(Filter $filter)
|
|
{
|
|
$this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add an additional filter expression to this query
|
|
*
|
|
* This notifies the repository about each required filter column.
|
|
*
|
|
* @param Filter $filter
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function addFilter(Filter $filter)
|
|
{
|
|
$this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return the current filter
|
|
*
|
|
* @return Filter
|
|
*/
|
|
public function getFilter()
|
|
{
|
|
return $this->query->getFilter();
|
|
}
|
|
|
|
/**
|
|
* Return the sort rules being applied on this query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSortRules()
|
|
{
|
|
return $this->repository->getSortRules($this->target);
|
|
}
|
|
|
|
/**
|
|
* Add a sort rule for this query
|
|
*
|
|
* If called without a specific column, the repository's defaul sort rules will be applied.
|
|
* This notifies the repository about each column being required as filter column.
|
|
*
|
|
* @param string $field The name of the column by which to sort the query's result
|
|
* @param string $direction The direction to use when sorting (asc or desc, default is asc)
|
|
* @param bool $ignoreDefault Whether to ignore any default sort rules if $field is given
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function order($field = null, $direction = null, $ignoreDefault = false)
|
|
{
|
|
$sortRules = $this->getSortRules();
|
|
if ($field === null) {
|
|
// Use first available sort rule as default
|
|
if (empty($sortRules)) {
|
|
// Return early in case of no sort defaults and no given $field
|
|
return $this;
|
|
}
|
|
|
|
$sortColumns = reset($sortRules);
|
|
if (! array_key_exists('columns', $sortColumns)) {
|
|
$sortColumns['columns'] = array(key($sortRules));
|
|
}
|
|
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
|
|
$sortColumns['order'] = $direction ?: static::SORT_ASC;
|
|
}
|
|
} else {
|
|
$alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field;
|
|
if (! $ignoreDefault && array_key_exists($alias, $sortRules)) {
|
|
$sortColumns = $sortRules[$alias];
|
|
if (! array_key_exists('columns', $sortColumns)) {
|
|
$sortColumns['columns'] = array($alias);
|
|
}
|
|
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
|
|
$sortColumns['order'] = $direction ?: static::SORT_ASC;
|
|
}
|
|
} else {
|
|
$sortColumns = array(
|
|
'columns' => array($alias),
|
|
'order' => $direction
|
|
);
|
|
}
|
|
}
|
|
|
|
$baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;
|
|
|
|
foreach ($sortColumns['columns'] as $column) {
|
|
list($column, $specificDirection) = $this->splitOrder($column);
|
|
|
|
if ($this->hasLimit() && $this->repository->providesValueConversion($this->target, $column)) {
|
|
Logger::debug(
|
|
'Cannot order by column "%s" in repository "%s". The query is'
|
|
. ' limited and applies value conversion rules on the column',
|
|
$column,
|
|
$this->repository->getName()
|
|
);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$this->query->order(
|
|
$this->repository->requireFilterColumn($this->target, $column, $this),
|
|
$specificDirection ?: $baseDirection
|
|
// I would have liked the following solution, but hey, a coder should be allowed to produce crap...
|
|
// $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
|
|
);
|
|
} catch (QueryException $_) {
|
|
Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Extract and return the name and direction of the given sort column definition
|
|
*
|
|
* @param string $field
|
|
*
|
|
* @return array An array of two items: $columnName, $direction
|
|
*/
|
|
protected function splitOrder($field)
|
|
{
|
|
$columnAndDirection = explode(' ', $field, 2);
|
|
if (count($columnAndDirection) === 1) {
|
|
$column = $field;
|
|
$direction = null;
|
|
} else {
|
|
$column = $columnAndDirection[0];
|
|
$direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
|
|
? static::SORT_DESC
|
|
: static::SORT_ASC;
|
|
}
|
|
|
|
return array($column, $direction);
|
|
}
|
|
|
|
/**
|
|
* Return whether any sort rules were applied to this query
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasOrder()
|
|
{
|
|
return $this->query->hasOrder();
|
|
}
|
|
|
|
/**
|
|
* Return the sort rules applied to this query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getOrder()
|
|
{
|
|
return $this->query->getOrder();
|
|
}
|
|
|
|
/**
|
|
* Set whether this query should peek ahead for more results
|
|
*
|
|
* Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will
|
|
* be removed from the result set. Note that this only applies when fetching multiple results of limited queries.
|
|
*
|
|
* @param bool $state
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function peekAhead($state = true)
|
|
{
|
|
$this->query->peekAhead($state);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return whether this query did not yield all available results
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasMore()
|
|
{
|
|
return $this->query->hasMore();
|
|
}
|
|
|
|
/**
|
|
* Return whether this query will or has yielded any result
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasResult()
|
|
{
|
|
return $this->query->hasResult();
|
|
}
|
|
|
|
/**
|
|
* Limit this query's results
|
|
*
|
|
* @param int $count When to stop returning results
|
|
* @param int $offset When to start returning results
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function limit($count = null, $offset = null)
|
|
{
|
|
$this->query->limit($count, $offset);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return whether this query does not return all available entries from its result
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasLimit()
|
|
{
|
|
return $this->query->hasLimit();
|
|
}
|
|
|
|
/**
|
|
* Return the limit when to stop returning results
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getLimit()
|
|
{
|
|
return $this->query->getLimit();
|
|
}
|
|
|
|
/**
|
|
* Return whether this query does not start returning results at the very first entry
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasOffset()
|
|
{
|
|
return $this->query->hasOffset();
|
|
}
|
|
|
|
/**
|
|
* Return the offset when to start returning results
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getOffset()
|
|
{
|
|
return $this->query->getOffset();
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first column of this query's first row
|
|
*
|
|
* @return mixed|false False in case of no result
|
|
*/
|
|
public function fetchOne()
|
|
{
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
$result = $this->query->fetchOne();
|
|
if ($result !== false && $this->repository->providesValueConversion($this->target)) {
|
|
$columns = $this->getColumns();
|
|
$column = isset($columns[0]) ? $columns[0] : $this->getNativeAlias(key($columns));
|
|
return $this->repository->retrieveColumn($this->target, $column, $result, $this);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first row of this query's result
|
|
*
|
|
* @return object|false False in case of no result
|
|
*/
|
|
public function fetchRow()
|
|
{
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
$result = $this->query->fetchRow();
|
|
if ($result !== false && $this->repository->providesValueConversion($this->target)) {
|
|
foreach ($this->getColumns() as $alias => $column) {
|
|
if (! is_string($alias)) {
|
|
$alias = $column;
|
|
}
|
|
|
|
$result->$alias = $this->repository->retrieveColumn(
|
|
$this->target,
|
|
$this->getNativeAlias($alias),
|
|
$result->$alias,
|
|
$this
|
|
);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first column of all rows of the result set as an array
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchColumn()
|
|
{
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
$results = $this->query->fetchColumn();
|
|
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
|
|
$columns = $this->getColumns();
|
|
$aliases = array_keys($columns);
|
|
$column = is_int($aliases[0]) ? $columns[0] : $this->getNativeAlias($aliases[0]);
|
|
if ($this->repository->providesValueConversion($this->target, $column)) {
|
|
foreach ($results as & $value) {
|
|
$value = $this->repository->retrieveColumn($this->target, $column, $value, $this);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Fetch and return all rows of this query's result set as an array of key-value pairs
|
|
*
|
|
* The first column is the key, the second column is the value.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchPairs()
|
|
{
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
$results = $this->query->fetchPairs();
|
|
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
|
|
$columns = $this->getColumns();
|
|
$aliases = array_keys($columns);
|
|
$colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0];
|
|
$colTwo = count($aliases) < 2 ? $colOne : (
|
|
$aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1]
|
|
);
|
|
|
|
if ($this->repository->providesValueConversion($this->target, $colOne)
|
|
|| $this->repository->providesValueConversion($this->target, $colTwo)
|
|
) {
|
|
$newResults = array();
|
|
foreach ($results as $colOneValue => $colTwoValue) {
|
|
$colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue, $this);
|
|
$newResults[$colOneValue] = $this->repository->retrieveColumn(
|
|
$this->target,
|
|
$colTwo,
|
|
$colTwoValue,
|
|
$this
|
|
);
|
|
}
|
|
|
|
$results = $newResults;
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Fetch and return all results of this query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchAll()
|
|
{
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
$results = $this->query->fetchAll();
|
|
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
|
|
$updateOrder = false;
|
|
$columns = $this->getColumns();
|
|
$flippedColumns = array_flip($columns);
|
|
foreach ($results as $row) {
|
|
foreach ($columns as $alias => $column) {
|
|
if (! is_string($alias)) {
|
|
$alias = $column;
|
|
}
|
|
|
|
$row->$alias = $this->repository->retrieveColumn(
|
|
$this->target,
|
|
$this->getNativeAlias($alias),
|
|
$row->$alias,
|
|
$this
|
|
);
|
|
}
|
|
|
|
foreach (($this->getOrder() ?: array()) as $rule) {
|
|
$nativeAlias = $this->getNativeAlias($rule[0]);
|
|
if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $rule[0])) {
|
|
if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
|
|
$updateOrder = true;
|
|
$row->{$rule[0]} = $this->repository->retrieveColumn(
|
|
$this->target,
|
|
$nativeAlias,
|
|
$row->{$rule[0]},
|
|
$this
|
|
);
|
|
}
|
|
} elseif (array_key_exists($rule[0], $flippedColumns)) {
|
|
if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
|
|
$updateOrder = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($updateOrder) {
|
|
uasort($results, array($this->query, 'compare'));
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Count all results of this query
|
|
*
|
|
* @return int
|
|
*/
|
|
public function count()
|
|
{
|
|
return $this->query->count();
|
|
}
|
|
|
|
/**
|
|
* Return the current position of this query's iterator
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getIteratorPosition()
|
|
{
|
|
return $this->query->getIteratorPosition();
|
|
}
|
|
|
|
/**
|
|
* Start or rewind the iteration
|
|
*/
|
|
public function rewind()
|
|
{
|
|
if ($this->iterator === null) {
|
|
if (! $this->hasOrder()) {
|
|
$this->order();
|
|
}
|
|
|
|
if ($this->query instanceof Traversable) {
|
|
$iterator = $this->query;
|
|
} else {
|
|
$iterator = $this->repository->getDataSource($this->target)->query($this->query);
|
|
}
|
|
|
|
if ($iterator instanceof IteratorAggregate) {
|
|
$this->iterator = $iterator->getIterator();
|
|
} else {
|
|
$this->iterator = $iterator;
|
|
}
|
|
}
|
|
|
|
$this->iterator->rewind();
|
|
Benchmark::measure('Query result iteration started');
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the current row of this query's result
|
|
*
|
|
* @return object
|
|
*/
|
|
public function current()
|
|
{
|
|
$row = $this->iterator->current();
|
|
if ($this->repository->providesValueConversion($this->target)) {
|
|
foreach ($this->getColumns() as $alias => $column) {
|
|
if (! is_string($alias)) {
|
|
$alias = $column;
|
|
}
|
|
|
|
$row->$alias = $this->repository->retrieveColumn(
|
|
$this->target,
|
|
$this->getNativeAlias($alias),
|
|
$row->$alias,
|
|
$this
|
|
);
|
|
}
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Return whether the current row of this query's result is valid
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function valid()
|
|
{
|
|
if (! $this->iterator->valid()) {
|
|
Benchmark::measure('Query result iteration finished');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return the key for the current row of this query's result
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function key()
|
|
{
|
|
return $this->iterator->key();
|
|
}
|
|
|
|
/**
|
|
* Advance to the next row of this query's result
|
|
*/
|
|
public function next()
|
|
{
|
|
$this->iterator->next();
|
|
}
|
|
}
|