Files
molenda.net/eeeeee/vendor/cakephp/database/Query/SelectQuery.php
T
Sebastian Molenda ab96d82fcf init
2026-05-12 21:10:38 +02:00

836 lines
26 KiB
PHP

<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.5.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Query;
use ArrayIterator;
use Cake\Core\Exception\CakeException;
use Cake\Database\Connection;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\WindowExpression;
use Cake\Database\ExpressionInterface;
use Cake\Database\Query;
use Cake\Database\StatementInterface;
use Cake\Database\TypeMap;
use Closure;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
use function Cake\Core\deprecationWarning;
/**
* This class is used to generate SELECT queries for the relational database.
*
* @template T of mixed
* @implements \IteratorAggregate<T>
*/
class SelectQuery extends Query implements IteratorAggregate
{
/**
* Type of this query.
*
* @var string
*/
protected string $_type = self::TYPE_SELECT;
/**
* List of SQL parts that will be used to build this query.
*
* @var array<string, mixed>
*/
protected array $_parts = [
'comment' => null,
'with' => [],
'select' => [],
'optimizerHint' => [],
'modifier' => [],
'distinct' => false,
'from' => [],
'join' => [],
'where' => null,
'group' => [],
'having' => null,
'window' => [],
'order' => null,
'limit' => null,
'offset' => null,
'union' => [],
'epilog' => null,
'intersect' => [],
];
/**
* A list of callbacks to be called to alter each row from resulting
* statement upon retrieval. Each one of the callback function will receive
* the row array as first argument.
*
* @var array<\Closure>
*/
protected array $_resultDecorators = [];
/**
* Result set from executed SELECT query.
*
* @var iterable|null
*/
protected ?iterable $_results = null;
/**
* Boolean for tracking whether buffered results
* are enabled.
*
* @var bool
*/
protected bool $bufferedResults = true;
/**
* The Type map for fields in the select clause
*
* @var \Cake\Database\TypeMap|null
*/
protected ?TypeMap $_selectTypeMap = null;
/**
* Tracking flag to disable casting
*
* @var bool
*/
protected bool $typeCastEnabled = true;
/**
* Executes query and returns set of decorated results.
*
* The results are cached until the query is modified and marked dirty.
*
* @return iterable
* @throws \Cake\Core\Exception\CakeException When query is not a SELECT query.
*/
public function all(): iterable
{
if ($this->_results === null || $this->_dirty) {
$this->_results = $this->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC);
}
return $this->_results;
}
/**
* Adds new fields to be returned by a `SELECT` statement when this query is
* executed. Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* If an array is passed, keys will be used to alias fields using the value as the
* real field to be aliased. It is possible to alias strings, Expression objects or
* even other Query objects.
*
* If a callback is passed, the returning array of the function will
* be used as the list of fields.
*
* By default this function will append any passed argument to the list of fields
* to be selected, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* $query->select(['id', 'title']); // Produces SELECT id, title
* $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author
* $query->select('id', true); // Resets the list: SELECT id
* $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total
* $query->select(function ($query) {
* return ['article_id', 'total' => $query->func()->count('*')];
* })
* ```
*
* By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append
* fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields
* from the table.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields fields to be added to the list.
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function select(ExpressionInterface|Closure|array|string|float|int $fields = [], bool $overwrite = false)
{
if (!is_string($fields) && $fields instanceof Closure) {
$fields = $fields($this);
}
if (!is_array($fields)) {
$fields = [$fields];
}
if ($overwrite) {
$this->_parts['select'] = $fields;
} else {
$this->_parts['select'] = array_merge($this->_parts['select'], $fields);
}
$this->_dirty();
return $this;
}
/**
* Adds a `DISTINCT` clause to the query to remove duplicates from the result set.
* This clause can only be used for select statements.
*
* If you wish to filter duplicates based of those rows sharing a particular field
* or set of fields, you may pass an array of fields to filter on. Beware that
* this option might not be fully supported in all database systems.
*
* ### Examples:
*
* ```
* // Filters products with the same name and city
* $query->select(['name', 'city'])->from('products')->distinct();
*
* // Filters products in the same city
* $query->distinct(['city']);
* $query->distinct('city');
*
* // Filter products with the same name
* $query->distinct(['name'], true);
* $query->distinct('name', true);
* ```
*
* @param \Cake\Database\ExpressionInterface|array|string|bool $on Enable/disable distinct class
* or list of fields to be filtered on
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function distinct(ExpressionInterface|array|string|bool $on = [], bool $overwrite = false)
{
if ($on === []) {
$on = true;
} elseif (is_string($on)) {
$on = [$on];
}
if (is_array($on)) {
$merge = [];
if (is_array($this->_parts['distinct'])) {
$merge = $this->_parts['distinct'];
}
$on = $overwrite ? array_values($on) : array_merge($merge, array_values($on));
}
$this->_parts['distinct'] = $on;
$this->_dirty();
return $this;
}
/**
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* By default this function will append any passed argument to the list of fields
* to be grouped, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* // Produces GROUP BY id, title
* $query->groupBy(['id', 'title']);
*
* // Produces GROUP BY title
* $query->groupBy('title');
* ```
*
* Group fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
* @deprecated 5.0.0 Use groupBy() instead now that CollectionInterface methods are no longer proxied.
*/
public function group(ExpressionInterface|array|string $fields, bool $overwrite = false)
{
deprecationWarning('5.0.0', 'SelectQuery::group() is deprecated. Use SelectQuery::groupBy() instead.');
return $this->groupBy($fields, $overwrite);
}
/**
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
* Fields can be passed as an array of strings, array of expression
* objects, a single expression or a single string.
*
* By default this function will append any passed argument to the list of fields
* to be grouped, unless the second argument is set to true.
*
* ### Examples:
*
* ```
* // Produces GROUP BY id, title
* $query->groupBy(['id', 'title']);
*
* // Produces GROUP BY title
* $query->groupBy('title');
* ```
*
* Group fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
* @param bool $overwrite whether to reset fields with passed list or not
* @return $this
*/
public function groupBy(ExpressionInterface|array|string $fields, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['group'] = [];
}
if (!is_array($fields)) {
$fields = [$fields];
}
$this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields));
$this->_dirty();
return $this;
}
/**
* Adds a condition or set of conditions to be used in the `HAVING` clause for this
* query. This method operates in exactly the same way as the method `where()`
* does. Please refer to its documentation for an insight on how to using each
* parameter.
*
* Having fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The having conditions.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @param bool $overwrite whether to reset conditions with passed list or not
* @see \Cake\Database\Query::where()
* @return $this
*/
public function having(
ExpressionInterface|Closure|array|string|null $conditions = null,
array $types = [],
bool $overwrite = false,
) {
if ($overwrite) {
$this->_parts['having'] = $this->expr();
}
$this->_conjugate('having', $conditions, 'AND', $types);
return $this;
}
/**
* Connects any previously defined set of conditions to the provided list
* using the AND operator in the HAVING clause. This method operates in exactly
* the same way as the method `andWhere()` does. Please refer to its
* documentation for an insight on how to using each parameter.
*
* Having fields are not suitable for use with user supplied data as they are
* not sanitized by the query builder.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The AND conditions for HAVING.
* @param array<string, string> $types Associative array of type names used to bind values to query
* @see \Cake\Database\Query::andWhere()
* @return $this
*/
public function andHaving(ExpressionInterface|Closure|array|string $conditions, array $types = [])
{
$this->_conjugate('having', $conditions, 'AND', $types);
return $this;
}
/**
* Adds a named window expression.
*
* You are responsible for adding windows in the order your database requires.
*
* @param string $name Window name
* @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression
* @param bool $overwrite Clear all previous query window expressions
* @return $this
*/
public function window(string $name, WindowExpression|Closure $window, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['window'] = [];
}
if ($window instanceof Closure) {
$window = $window(new WindowExpression(), $this);
if (!($window instanceof WindowExpression)) {
throw new CakeException('You must return a `WindowExpression` from a Closure passed to `window()`.');
}
}
$this->_parts['window'][] = ['name' => new IdentifierExpression($name), 'window' => $window];
$this->_dirty();
return $this;
}
/**
* Set the page of results you want.
*
* This method provides an easier to use interface to set the limit + offset
* in the record set you want as results. If empty the limit will default to
* the existing limit clause, and if that too is empty, then `25` will be used.
*
* Pages must start at 1.
*
* @param int $num The page number you want.
* @param int|null $limit The number of rows you want in the page. If null
* the current limit clause will be used.
* @return $this
* @throws \InvalidArgumentException If page number < 1.
*/
public function page(int $num, ?int $limit = null)
{
if ($num < 1) {
throw new InvalidArgumentException('Pages must start at 1.');
}
if ($limit !== null) {
$this->limit($limit);
}
$limit = $this->clause('limit');
if ($limit === null) {
$limit = 25;
$this->limit($limit);
}
$offset = ($num - 1) * $limit;
if (PHP_INT_MAX <= $offset) {
$offset = PHP_INT_MAX;
}
$this->offset((int)$offset);
return $this;
}
/**
* Adds a complete query to be used in conjunction with an UNION operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* By default, the UNION operator will remove duplicate rows, if you wish to include
* every row for all queries, use unionAll().
*
* ### Examples
*
* ```
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->union($union);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d UNION SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function union(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['union'] = [];
}
$this->_parts['union'][] = [
'all' => false,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with the UNION ALL operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* Unlike UNION, UNION ALL will not remove duplicate rows.
*
* ```
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function unionAll(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['union'] = [];
}
$this->_parts['union'][] = [
'all' => true,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with an INTERSECT operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* By default, the INTERSECT operator will remove duplicate rows, if you wish to include
* every row for all queries, use intersectAll().
*
* ### Examples
*
* ```
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersect($intersect);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d INTERSECT SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function intersect(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['intersect'] = [];
}
$this->_parts['intersect'][] = [
'all' => false,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Adds a complete query to be used in conjunction with the INTERSECT ALL operator with
* this query. This is used to combine the result set of this query with the one
* that will be returned by the passed query. You can add as many queries as you
* required by calling multiple times this method with different queries.
*
* Unlike INTERSECT, INTERSECT ALL will not remove duplicate rows.
*
* ```
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersectAll($intersect);
* ```
*
* Will produce:
*
* `SELECT id, name FROM things d INTERSECT ALL SELECT id, title FROM articles a`
*
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
* @param bool $overwrite whether to reset the list of queries to be operated or not
* @return $this
*/
public function intersectAll(Query|string $query, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['intersect'] = [];
}
$this->_parts['intersect'][] = [
'all' => true,
'query' => $query,
];
$this->_dirty();
return $this;
}
/**
* Executes this query and returns a results iterator. This function is required
* for implementing the IteratorAggregate interface and allows the query to be
* iterated without having to call all() manually, thus making it look like
* a result set instead of the query itself.
*
* @return \Traversable
*/
public function getIterator(): Traversable
{
if ($this->bufferedResults) {
/** @var \Traversable|array $results */
$results = $this->all();
if (is_array($results)) {
return new ArrayIterator($results);
}
return $results;
}
return $this->execute();
}
/**
* Registers a callback to be executed for each result that is fetched from the
* result set, the callback function will receive as first parameter an array with
* the raw data from the database for every row that is fetched and must return the
* row with any possible modifications.
*
* Callbacks will be executed lazily, if only 3 rows are fetched for database it will
* be called 3 times, event though there might be more rows to be fetched in the cursor.
*
* Callbacks are stacked in the order they are registered, if you wish to reset the stack
* the call this function with the second parameter set to true.
*
* If you wish to remove all decorators from the stack, set the first parameter
* to null and the second to true.
*
* ### Example
*
* ```
* $query->decorateResults(function ($row) {
* $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']);
* return $row;
* });
* ```
*
* @param \Closure|null $callback The callback to invoke when results are fetched.
* @param bool $overwrite Whether this should append or replace all existing decorators.
* @return $this
*/
public function decorateResults(?Closure $callback, bool $overwrite = false)
{
$this->_dirty();
if ($overwrite) {
$this->_resultDecorators = [];
}
if ($callback !== null) {
$this->_resultDecorators[] = $callback;
}
return $this;
}
/**
* Get result decorators.
*
* @return array
*/
public function getResultDecorators(): array
{
return $this->_resultDecorators;
}
/**
* Enables buffered results.
*
* When enabled the results returned by this query will be
* buffered. This enables you to iterate a result set multiple times, or
* both cache and iterate it.
*
* When disabled it will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return $this
*/
public function enableBufferedResults()
{
$this->_dirty();
$this->bufferedResults = true;
return $this;
}
/**
* Disables buffered results.
*
* Disabling buffering will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return $this
*/
public function disableBufferedResults()
{
$this->_dirty();
$this->bufferedResults = false;
return $this;
}
/**
* Returns whether buffered results are enabled/disabled.
*
* When enabled the results returned by this query will be
* buffered. This enables you to iterate a result set multiple times, or
* both cache and iterate it.
*
* When disabled it will consume less memory as fetched results are not
* remembered for future iterations.
*
* @return bool
*/
public function isBufferedResultsEnabled(): bool
{
return $this->bufferedResults;
}
/**
* Sets the TypeMap class where the types for each of the fields in the
* select clause are stored.
*
* @param \Cake\Database\TypeMap|array $typeMap Creates a TypeMap if array, otherwise sets the given TypeMap.
* @return $this
*/
public function setSelectTypeMap(TypeMap|array $typeMap)
{
$this->_selectTypeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap;
$this->_dirty();
return $this;
}
/**
* Gets the TypeMap class where the types for each of the fields in the
* select clause are stored.
*
* @return \Cake\Database\TypeMap
*/
public function getSelectTypeMap(): TypeMap
{
return $this->_selectTypeMap ??= new TypeMap();
}
/**
* Disables result casting.
*
* When disabled, the fields will be returned as received from the database
* driver (which in most environments means they are being returned as
* strings), which can improve performance with larger datasets.
*
* @return $this
*/
public function disableResultsCasting()
{
$this->typeCastEnabled = false;
return $this;
}
/**
* Enables result casting.
*
* When enabled, the fields in the results returned by this Query will be
* cast to their corresponding PHP data type.
*
* @return $this
*/
public function enableResultsCasting()
{
$this->typeCastEnabled = true;
return $this;
}
/**
* Returns whether result casting is enabled/disabled.
*
* When enabled, the fields in the results returned by this Query will be
* casted to their corresponding PHP data type.
*
* When disabled, the fields will be returned as received from the database
* driver (which in most environments means they are being returned as
* strings), which can improve performance with larger datasets.
*
* @return bool
*/
public function isResultsCastingEnabled(): bool
{
return $this->typeCastEnabled;
}
/**
* Handles clearing iterator and cloning all expressions and value binders.
*
* @return void
*/
public function __clone()
{
parent::__clone();
$this->_results = null;
if ($this->_selectTypeMap !== null) {
$this->_selectTypeMap = clone $this->_selectTypeMap;
}
}
/**
* Returns an array that can be used to describe the internal state of this
* object.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{
$return = parent::__debugInfo();
$return['decorators'] = count($this->_resultDecorators);
return $return;
}
/**
* Sets the connection role.
*
* @param string $role Connection role ('read' or 'write')
* @return $this
*/
public function setConnectionRole(string $role)
{
assert($role === Connection::ROLE_READ || $role === Connection::ROLE_WRITE);
$this->connectionRole = $role;
return $this;
}
/**
* Sets the connection role to read.
*
* @return $this
*/
public function useReadRole()
{
return $this->setConnectionRole(Connection::ROLE_READ);
}
/**
* Sets the connection role to write.
*
* @return $this
*/
public function useWriteRole()
{
return $this->setConnectionRole(Connection::ROLE_WRITE);
}
}