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 = isset($sortColumns['order']) && 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(): int { 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(): void { 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 mixed */ #[\ReturnTypeWillChange] 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(): bool { 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 */ #[\ReturnTypeWillChange] public function key() { return $this->iterator->key(); } /** * Advance to the next row of this query's result */ public function next(): void { $this->iterator->next(); } }