Files
espocrm/application/Espo/Core/Select/SelectBuilder.php
2026-01-19 17:46:06 +01:00

576 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 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\Core\Select;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\Applier\Factory as ApplierFactory;
use Espo\Core\Select\Where\Params as WhereParams;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Select\Order\Params as OrderParams;
use Espo\Core\Select\Text\FilterParams as TextFilterParams;
use Espo\Core\Select\Where\Applier as WhereApplier;
use Espo\Core\Select\Select\Applier as SelectApplier;
use Espo\Core\Select\Order\Applier as OrderApplier;
use Espo\Core\Select\AccessControl\Applier as AccessControlFilterApplier;
use Espo\Core\Select\Primary\Applier as PrimaryFilterApplier;
use Espo\Core\Select\Bool\Applier as BoolFilterListApplier;
use Espo\Core\Select\Text\Applier as TextFilterApplier;
use Espo\Core\Select\Applier\Appliers\Limit as LimitApplier;
use Espo\Core\Select\Applier\Appliers\Additional as AdditionalApplier;
use Espo\ORM\Query\Select as Query;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
use Espo\Entities\User;
use LogicException;
/**
* Builds select queries for ORM. Applies search parameters(passed from front-end),
* ACL restrictions, filters, etc.
*/
class SelectBuilder
{
private ?string $entityType = null;
private ?OrmSelectBuilder $queryBuilder = null;
private ?Query $sourceQuery = null;
private ?SearchParams $searchParams = null;
private bool $applyAccessControlFilter = false;
private bool $applyDefaultOrder = false;
private ?string $textFilter = null;
private ?string $primaryFilter = null;
/** @var string[] */
private array $boolFilterList = [];
/** @var WhereItem[] */
private array $whereItemList = [];
private bool $applyWherePermissionCheck = false;
private bool $applyComplexExpressionsForbidden = false;
/** @var class-string<Applier\AdditionalApplier>[] */
private array $additionalApplierClassNameList = [];
public function __construct(
private User $user,
private ApplierFactory $applierFactory
) {}
/**
* Specify an entity type to select from.
*/
public function from(string $entityType): self
{
if ($this->sourceQuery) {
throw new LogicException("Can't call 'from' after 'clone'.");
}
$this->entityType = $entityType;
return $this;
}
/**
* Start building from an existing select query.
*/
public function clone(Query $query): self
{
if ($this->entityType && $this->entityType !== $query->getFrom()) {
throw new LogicException("Not matching entity type.");
}
$this->entityType = $query->getFrom();
$this->sourceQuery = $query;
return $this;
}
/**
* Build a result query.
*
* @throws Forbidden
* @throws BadRequest
*/
public function build(): Query
{
return $this->buildQueryBuilder()->build();
}
/**
* Build an ORM query builder. Used to continue building but by means of ORM.
*
* @throws Forbidden
* @throws BadRequest
*/
public function buildQueryBuilder(): QueryBuilder
{
$this->queryBuilder = new OrmSelectBuilder();
if (!$this->entityType) {
throw new LogicException("No entity type.");
}
if ($this->sourceQuery) {
$this->queryBuilder->clone($this->sourceQuery);
} else {
$this->queryBuilder->from($this->entityType);
}
$this->applyFromSearchParams();
if (count($this->whereItemList)) {
$this->applyWhereItemList();
}
if ($this->applyDefaultOrder) {
$this->applyDefaultOrder();
}
if ($this->primaryFilter) {
$this->applyPrimaryFilter();
}
if (count($this->boolFilterList)) {
$this->applyBoolFilterList();
}
if ($this->textFilter) {
$this->applyTextFilter();
}
if ($this->applyAccessControlFilter) {
$this->applyAccessControlFilter();
}
$this->applyAdditional();
return $this->queryBuilder ?? throw new LogicException();
}
/**
* Switch a user for whom a select query will be built.
*/
public function forUser(User $user): self
{
$this->user = $user;
return $this;
}
/**
* Apply search parameters.
*
* Note: If there's no order set in the search parameters then a default order will be applied.
*/
public function withSearchParams(SearchParams $searchParams): self
{
$this->searchParams = $searchParams;
$this->withBoolFilterList(
$searchParams->getBoolFilterList()
);
$primaryFilter = $searchParams->getPrimaryFilter();
if ($primaryFilter) {
$this->withPrimaryFilter($primaryFilter);
}
$textFilter = $searchParams->getTextFilter();
if ($textFilter !== null) {
$this->withTextFilter($textFilter);
}
return $this;
}
/**
* Apply maximum restrictions for a user.
*/
public function withStrictAccessControl(): self
{
$this->withAccessControlFilter();
$this->withWherePermissionCheck();
$this->withComplexExpressionsForbidden();
return $this;
}
/**
* Apply an access control filter.
*/
public function withAccessControlFilter(): self
{
$this->applyAccessControlFilter = true;
return $this;
}
/**
* Apply a default order.
*/
public function withDefaultOrder(): self
{
$this->applyDefaultOrder = true;
return $this;
}
/**
* Check permissions to where items.
*/
public function withWherePermissionCheck(): self
{
$this->applyWherePermissionCheck = true;
return $this;
}
/**
* Forbid complex expression usage.
*/
public function withComplexExpressionsForbidden(): self
{
$this->applyComplexExpressionsForbidden = true;
return $this;
}
/**
* Apply a text filter.
*/
public function withTextFilter(string $textFilter): self
{
$this->textFilter = $textFilter;
return $this;
}
/**
* Apply a primary filter.
*/
public function withPrimaryFilter(string $primaryFilter): self
{
$this->primaryFilter = $primaryFilter;
return $this;
}
/**
* Apply a bool filter.
*/
public function withBoolFilter(string $boolFilter): self
{
$this->boolFilterList[] = $boolFilter;
return $this;
}
/**
* Apply a list of bool filters.
*
* @param string[] $boolFilterList
*/
public function withBoolFilterList(array $boolFilterList): self
{
$this->boolFilterList = array_merge($this->boolFilterList, $boolFilterList);
return $this;
}
/**
* Apply a Where Item.
*/
public function withWhere(WhereItem $whereItem): self
{
$this->whereItemList[] = $whereItem;
return $this;
}
/**
* Apply a list of additional applier class names.
*
* @param class-string<Applier\AdditionalApplier>[] $additionalApplierClassNameList
*/
public function withAdditionalApplierClassNameList(array $additionalApplierClassNameList): self
{
$this->additionalApplierClassNameList = array_merge(
$this->additionalApplierClassNameList,
$additionalApplierClassNameList
);
return $this;
}
/**
* @throws BadRequest
*/
private function applyPrimaryFilter(): void
{
assert($this->queryBuilder !== null);
assert($this->primaryFilter !== null);
$this->createPrimaryFilterApplier()
->apply(
$this->queryBuilder,
$this->primaryFilter
);
}
/**
* @throws BadRequest
*/
private function applyBoolFilterList(): void
{
assert($this->queryBuilder !== null);
$this->createBoolFilterListApplier()
->apply(
$this->queryBuilder,
$this->boolFilterList
);
}
private function applyTextFilter(): void
{
assert($this->queryBuilder !== null);
assert($this->textFilter !== null);
$this->createTextFilterApplier()
->apply(
$this->queryBuilder,
$this->textFilter,
TextFilterParams::create()
);
}
private function applyAccessControlFilter(): void
{
assert($this->queryBuilder !== null);
$this->createAccessControlFilterApplier()
->apply(
$this->queryBuilder
);
}
/**
* @throws Forbidden
* @throws BadRequest
*/
private function applyDefaultOrder(): void
{
assert($this->queryBuilder !== null);
$order = $this->searchParams?->getOrder();
$params = OrderParams::fromAssoc([
'forceDefault' => true,
'order' => $order,
]);
$this->createOrderApplier()
->apply(
$this->queryBuilder,
$params
);
}
/**
* @throws BadRequest
* @throws Forbidden
*/
private function applyWhereItemList(): void
{
foreach ($this->whereItemList as $whereItem) {
$this->applyWhereItem($whereItem);
}
}
/**
* @throws BadRequest
* @throws Forbidden
*/
private function applyWhereItem(WhereItem $whereItem): void
{
assert($this->queryBuilder !== null);
$params = WhereParams::fromAssoc([
'applyPermissionCheck' => $this->applyWherePermissionCheck,
'forbidComplexExpressions' => $this->applyComplexExpressionsForbidden,
]);
$this->createWhereApplier()
->apply(
$this->queryBuilder,
$whereItem,
$params
);
}
/**
* @throws Forbidden
* @throws BadRequest
*/
private function applyFromSearchParams(): void
{
if (!$this->searchParams) {
return;
}
assert($this->queryBuilder !== null);
if (
!$this->applyDefaultOrder &&
($this->searchParams->getOrderBy() || $this->searchParams->getOrder())
) {
$params = OrderParams::fromAssoc([
'orderBy' => $this->searchParams->getOrderBy(),
'order' => $this->searchParams->getOrder(),
'applyPermissionCheck' => $this->applyWherePermissionCheck,
]);
$this->createOrderApplier()
->apply(
$this->queryBuilder,
$params
);
}
if (!$this->searchParams->getOrderBy() && !$this->searchParams->getOrder()) {
$this->withDefaultOrder();
}
if ($this->searchParams->getMaxSize() !== null || $this->searchParams->getOffset() !== null) {
$this->createLimitApplier()
->apply(
$this->queryBuilder,
$this->searchParams->getOffset(),
$this->searchParams->getMaxSize()
);
}
if ($this->searchParams->getSelect()) {
$this->createSelectApplier()
->apply(
$this->queryBuilder,
$this->searchParams
);
}
if ($this->searchParams->getWhere()) {
$this->whereItemList[] = $this->searchParams->getWhere();
}
}
private function applyAdditional(): void
{
assert($this->queryBuilder !== null);
$searchParams = SearchParams::create()
->withBoolFilterList($this->boolFilterList)
->withPrimaryFilter($this->primaryFilter)
->withTextFilter($this->textFilter);
if ($this->searchParams) {
$searchParams = SearchParams::merge($searchParams, $this->searchParams);
}
$this->createAdditionalApplier()->apply(
$this->additionalApplierClassNameList,
$this->queryBuilder,
$searchParams
);
}
private function createWhereApplier(): WhereApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createWhere($this->entityType, $this->user);
}
private function createSelectApplier(): SelectApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createSelect($this->entityType, $this->user);
}
private function createOrderApplier(): OrderApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createOrder($this->entityType, $this->user);
}
private function createLimitApplier(): LimitApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createLimit($this->entityType, $this->user);
}
private function createAccessControlFilterApplier(): AccessControlFilterApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createAccessControlFilter($this->entityType, $this->user);
}
private function createTextFilterApplier(): TextFilterApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createTextFilter($this->entityType, $this->user);
}
private function createPrimaryFilterApplier(): PrimaryFilterApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createPrimaryFilter($this->entityType, $this->user);
}
private function createBoolFilterListApplier(): BoolFilterListApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createBoolFilterList($this->entityType, $this->user);
}
private function createAdditionalApplier(): AdditionalApplier
{
assert($this->entityType !== null);
return $this->applierFactory->createAdditional($this->entityType, $this->user);
}
}