- Updated copyright headers in 3,055 core application files - Changed 'Copyright (C) 2014-2025' to 'Copyright (C) 2014-2026' - Added 123 new files from EspoCRM core updates - Removed 4 deprecated files - Total changes: 61,637 insertions, 54,283 deletions This is a routine maintenance update for the new year 2026.
352 lines
9.8 KiB
PHP
352 lines
9.8 KiB
PHP
<?php
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
namespace Espo\ORM\Query;
|
||
|
||
use Espo\ORM\Query\Part\Expression;
|
||
use Espo\ORM\Query\Part\Selection;
|
||
use Espo\ORM\Query\Part\WhereItem;
|
||
|
||
use InvalidArgumentException;
|
||
use RuntimeException;
|
||
|
||
class SelectBuilder implements Builder
|
||
{
|
||
use SelectingBuilderTrait;
|
||
|
||
/**
|
||
* Create an instance.
|
||
*/
|
||
public static function create(): self
|
||
{
|
||
return new self();
|
||
}
|
||
|
||
/**
|
||
* Build a SELECT query.
|
||
*/
|
||
public function build(): Select
|
||
{
|
||
return Select::fromRaw($this->params);
|
||
}
|
||
|
||
/**
|
||
* Clone an existing query for a subsequent modifying and building.
|
||
*/
|
||
public function clone(Select $query): self
|
||
{
|
||
$this->cloneInternal($query);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Set FROM. For what entity type to build a query.
|
||
*/
|
||
public function from(string $entityType, ?string $alias = null): self
|
||
{
|
||
if (isset($this->params['from']) && $entityType !== $this->params['from']) {
|
||
throw new RuntimeException("Method 'from' can be called only once.");
|
||
}
|
||
|
||
if (isset($this->params['fromQuery'])) {
|
||
throw new RuntimeException("Method 'from' can't be if 'fromQuery' is set.");
|
||
}
|
||
|
||
$this->params['from'] = $entityType;
|
||
|
||
if ($alias) {
|
||
$this->params['fromAlias'] = $alias;
|
||
}
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Set FROM sub-query.
|
||
*/
|
||
public function fromQuery(SelectingQuery $query, string $alias): self
|
||
{
|
||
if (isset($this->params['from'])) {
|
||
throw new RuntimeException("Method 'fromQuery' can be called only once.");
|
||
}
|
||
|
||
if (isset($this->params['fromQuery'])) {
|
||
throw new RuntimeException("Method 'fromQuery' can't be if 'from' is set.");
|
||
}
|
||
|
||
if ($alias === '') {
|
||
throw new RuntimeException("Alias can't be empty.");
|
||
}
|
||
|
||
$this->params['fromQuery'] = $query;
|
||
$this->params['fromAlias'] = $alias;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Set DISTINCT parameter.
|
||
*/
|
||
public function distinct(): self
|
||
{
|
||
$this->params['distinct'] = true;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Apply OFFSET and LIMIT.
|
||
*/
|
||
public function limit(?int $offset = null, ?int $limit = null): self
|
||
{
|
||
$this->params['offset'] = $offset;
|
||
$this->params['limit'] = $limit;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Specify SELECT. Columns and expressions to be selected. If not called, then
|
||
* all entity attributes will be selected. Passing an array will reset
|
||
* previously set items. Passing a SelectExpression|Expression|string will append the item.
|
||
*
|
||
* Usage options:
|
||
* * `select(SelectExpression $expression)`
|
||
* * `select([$expr1, $expr2, ...])`
|
||
* * `select(string $expression, string $alias)`
|
||
*
|
||
* @param Selection|Selection[]|Expression|Expression[]|string[]|string|array<int, string[]|string> $select
|
||
* An array of expressions or one expression.
|
||
* @param string|null $alias An alias. Actual if the first parameter is not an array.
|
||
*/
|
||
public function select($select, ?string $alias = null): self
|
||
{
|
||
/** @phpstan-var mixed $select */
|
||
|
||
if (is_array($select)) {
|
||
$this->params['select'] = $this->normalizeSelectExpressionArray($select);
|
||
|
||
return $this;
|
||
}
|
||
|
||
if ($select instanceof Expression) {
|
||
$select = $select->getValue();
|
||
} else if ($select instanceof Selection) {
|
||
$alias = $alias ?? $select->getAlias();
|
||
$select = $select->getExpression()->getValue();
|
||
}
|
||
|
||
if (is_string($select)) {
|
||
$this->params['select'] = $this->params['select'] ?? [];
|
||
|
||
$this->params['select'][] = $alias !== null ?
|
||
[$select, $alias] :
|
||
$select;
|
||
|
||
return $this;
|
||
}
|
||
|
||
throw new InvalidArgumentException();
|
||
}
|
||
|
||
/**
|
||
* Specify GROUP BY.
|
||
* Passing an array will reset previously set items.
|
||
* Passing a string|Expression will append an item.
|
||
*
|
||
* Usage options:
|
||
* * `groupBy(Expression|string $expression)`
|
||
* * `groupBy([$expr1, $expr2, ...])`
|
||
*
|
||
* @param Expression|Expression[]|string|string[] $groupBy
|
||
*/
|
||
public function group($groupBy): self
|
||
{
|
||
/** @phpstan-var mixed $groupBy */
|
||
|
||
if (is_array($groupBy)) {
|
||
$this->params['groupBy'] = $this->normalizeExpressionItemArray($groupBy);
|
||
|
||
return $this;
|
||
}
|
||
|
||
if ($groupBy instanceof Expression) {
|
||
$groupBy = $groupBy->getValue();
|
||
}
|
||
|
||
if (is_string($groupBy)) {
|
||
$this->params['groupBy'] = $this->params['groupBy'] ?? [];
|
||
|
||
$this->params['groupBy'][] = $groupBy;
|
||
|
||
return $this;
|
||
}
|
||
|
||
throw new InvalidArgumentException();
|
||
}
|
||
|
||
/**
|
||
* @deprecated Use `group` method.
|
||
* @param Expression|Expression[]|string|string[] $groupBy
|
||
*/
|
||
public function groupBy($groupBy): self
|
||
{
|
||
return $this->group($groupBy);
|
||
}
|
||
|
||
/**
|
||
* Use index.
|
||
*/
|
||
public function useIndex(string $index): self
|
||
{
|
||
$this->params['useIndex'] = $this->params['useIndex'] ?? [];
|
||
|
||
$this->params['useIndex'][] = $index;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Add a HAVING clause.
|
||
*
|
||
* Usage options:
|
||
* * `having(WhereItem $clause)`
|
||
* * `having(array $clause)`
|
||
* * `having(string $key, string $value)`
|
||
*
|
||
* @param WhereItem|array<int|string, mixed>|string $clause A key or where clause.
|
||
* @param mixed[]|scalar|null $value A value. Omitted if the first argument is not string.
|
||
*/
|
||
public function having($clause, $value = null): self
|
||
{
|
||
$this->applyWhereClause('havingClause', $clause, $value);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Lock selected rows in shared mode. To be used within a transaction.
|
||
*/
|
||
public function forShare(): self
|
||
{
|
||
if (isset($this->params['forUpdate'])) {
|
||
throw new RuntimeException("Can't use two lock modes together.");
|
||
}
|
||
|
||
$this->params['forShare'] = true;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Lock selected rows. To be used within a transaction.
|
||
*/
|
||
public function forUpdate(): self
|
||
{
|
||
if (isset($this->params['forShare'])) {
|
||
throw new RuntimeException("Can't use two lock modes together.");
|
||
}
|
||
|
||
$this->params['forUpdate'] = true;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* @todo Remove?
|
||
*/
|
||
public function withDeleted(): self
|
||
{
|
||
$this->params['withDeleted'] = true;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Add an item to a with recursive statement.
|
||
*
|
||
* @param Union $query A query.
|
||
* @param string $name A CTE name. In UpperCamelCase.
|
||
* @since 9.3.0
|
||
*/
|
||
public function withRecursive(Union $query, string $name): self
|
||
{
|
||
$this->params['withRecursive'] ??= [];
|
||
|
||
$this->params['withRecursive'][] = [$query, $name];
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* @param array<Expression|Selection|mixed[]> $itemList
|
||
* @return array<array{0: string, 1?: string}|string>
|
||
*/
|
||
private function normalizeSelectExpressionArray(array $itemList): array
|
||
{
|
||
$resultList = [];
|
||
|
||
foreach ($itemList as $item) {
|
||
if ($item instanceof Expression) {
|
||
$resultList[] = $item->getValue();
|
||
|
||
continue;
|
||
}
|
||
|
||
if ($item instanceof Selection) {
|
||
$resultList[] = $item->getAlias() !== null ?
|
||
[$item->getExpression()->getValue(), $item->getAlias()] :
|
||
[$item->getExpression()->getValue()];
|
||
|
||
continue;
|
||
}
|
||
|
||
if (!is_array($item) || !count($item) || !$item[0] instanceof Expression) {
|
||
/** @var array{0:string,1?:string} $item */
|
||
$resultList[] = $item;
|
||
|
||
continue;
|
||
}
|
||
|
||
$newItem = [$item[0]->getValue()];
|
||
|
||
if (count($item) > 1) {
|
||
$newItem[] = $item[1];
|
||
}
|
||
|
||
/** @var array{0: string, 1?: string} $newItem */
|
||
|
||
$resultList[] = $newItem;
|
||
}
|
||
|
||
return $resultList;
|
||
}
|
||
}
|