Files
espocrm/application/Espo/Core/Select/Where/ItemGeneralConverter.php
bsiggel 127fa6503b chore: Update copyright year from 2025 to 2026 across core files
- 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.
2026-02-07 16:05:21 +01:00

1714 lines
52 KiB
PHP
Raw Permalink 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-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\Core\Select\Where;
use DateTimeZone;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Field\DateTime;
use Espo\Core\Select\Where\Item\Data;
use Espo\Core\Select\Where\Item\Type;
use Espo\Core\Select\Helpers\RandomStringGenerator;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Utils\Metadata;
use Espo\Entities\ArrayValue;
use Espo\Entities\User;
use Espo\ORM\Defs as ORMDefs;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Entity;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Join;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
use Espo\ORM\Query\Select;
use Espo\ORM\Query\SelectBuilder;
use Exception;
use RuntimeException;
/**
* Converts a where item to a where clause (for ORM).
*/
class ItemGeneralConverter implements ItemConverter
{
public function __construct(
private string $entityType,
private User $user,
private DateTimeItemTransformer $dateTimeItemTransformer,
private Scanner $scanner,
private ItemConverterFactory $itemConverterFactory,
private RandomStringGenerator $randomStringGenerator,
private ORMDefs $ormDefs,
private Config $config,
private Metadata $metadata,
private Config\ApplicationConfig $applicationConfig,
) {}
/**
* @throws BadRequest
*/
public function convert(SelectBuilder $queryBuilder, Item $item): WhereClauseItem
{
$type = $item->getType();
$value = $item->getValue();
$attribute = $item->getAttribute();
$data = $item->getData();
if ($data instanceof Item\Data\DateTime) {
return $this->convert(
$queryBuilder,
$this->dateTimeItemTransformer->transform($item)
);
}
if (!$type) {
throw new BadRequest("Bad where item. No 'type'.");
}
if (
$attribute &&
$this->itemConverterFactory->has($this->entityType, $attribute, $type)
) {
$converter = $this->itemConverterFactory->create(
$this->entityType,
$attribute,
$type,
$this->user
);
return $converter->convert($queryBuilder, $item);
}
switch ($type) {
case Type::OR:
case Type::AND:
return WhereClause::fromRaw($this->groupProcessAndOr($queryBuilder, $type, $value));
case Type::NOT:
case Type::SUBQUERY_NOT_IN:
case Type::SUBQUERY_IN:
return WhereClause::fromRaw($this->groupProcessSubQuery($type, $value));
}
if (!$attribute) {
throw new BadRequest("Bad where item. No 'attribute'.");
}
switch ($type) {
// @todo Revise. Maybe to use regular like, in, etc. types?
case 'columnLike':
case 'columnIn':
case 'columnNotIn':
case 'columnIsNull':
case 'columnIsNotNull':
case 'columnEquals':
case 'columnNotEquals':
return WhereClause::fromRaw($this->groupProcessColumn($type, $attribute, $value));
case Type::ARRAY_ANY_OF:
case Type::ARRAY_NONE_OF:
case Type::ARRAY_IS_EMPTY:
case Type::ARRAY_ALL_OF:
case Type::ARRAY_IS_NOT_EMPTY:
return WhereClause::fromRaw($this->groupProcessArray($queryBuilder, $type, $attribute, $value));
}
if ($type === Type::IN) {
return WhereClause::fromRaw($this->processIn($attribute, $value));
}
if ($type === Type::NOT_IN) {
return WhereClause::fromRaw($this->processNotIn($attribute, $value));
}
if ($type === Type::IS_LINKED_WITH) {
return WhereClause::fromRaw($this->processLinkedWith($attribute, $value));
}
if ($type === Type::IS_NOT_LINKED_WITH) {
return WhereClause::fromRaw($this->processNotLinkedWith($attribute, $value));
}
if ($type === Type::IS_LINKED_WITH_ALL) {
return WhereClause::fromRaw($this->processLinkedWithAll($attribute, $value));
}
if ($type === Type::IS_LINKED_WITH_ANY) {
return WhereClause::fromRaw($this->processIsLinked($attribute));
}
if ($type === Type::IS_LINKED_WITH_NONE) {
return WhereClause::fromRaw($this->processIsNotLinked($attribute));
}
if ($type === Type::EXPRESSION) {
return WhereClause::fromRaw($this->processExpression($attribute));
}
if ($type === Type::EQUALS) {
return WhereClause::fromRaw($this->processEquals($attribute, $value));
}
if ($type === Type::NOT_EQUALS) {
return WhereClause::fromRaw($this->processNotEquals($attribute, $value));
}
if ($type === Type::ON) {
return WhereClause::fromRaw($this->processOn($attribute, $value));
}
if ($type === Type::NOT_ON) {
return WhereClause::fromRaw($this->processNotOn($attribute, $value));
}
if ($type === Type::EVER) {
return WhereClause::fromRaw($this->processEver($attribute));
}
if ($type === Type::TODAY) {
return WhereClause::fromRaw($this->processToday($attribute, $item->getData()));
}
if ($type === Type::PAST) {
return WhereClause::fromRaw($this->processPast($attribute, $item->getData()));
}
if ($type === Type::FUTURE) {
return WhereClause::fromRaw($this->processFuture($attribute, $item->getData()));
}
if ($type === Type::LAST_SEVEN_DAYS) {
return WhereClause::fromRaw($this->processLastSevenDays($attribute, $item->getData()));
}
if ($type === Type::LAST_X_DAYS) {
return WhereClause::fromRaw($this->processLastXDays($attribute, $value, $item->getData()));
}
if ($type === Type::NEXT_X_DAYS) {
return WhereClause::fromRaw($this->processNextXDays($attribute, $value, $item->getData()));
}
if ($type === Type::OLDER_THAN_X_DAYS) {
return WhereClause::fromRaw($this->processOlderThanXDays($attribute, $value, $item->getData()));
}
if ($type === Type::AFTER_X_DAYS) {
return WhereClause::fromRaw($this->processAfterXDays($attribute, $value, $item->getData()));
}
if ($type === Type::CURRENT_MONTH) {
return WhereClause::fromRaw($this->processCurrentMonth($attribute));
}
if ($type === Type::LAST_MONTH) {
return WhereClause::fromRaw($this->processLastMonth($attribute));
}
if ($type === Type::NEXT_MONTH) {
return WhereClause::fromRaw($this->processNextMonth($attribute));
}
if ($type === Type::CURRENT_QUARTER) {
return WhereClause::fromRaw($this->processCurrentQuarter($attribute));
}
if ($type === Type::LAST_QUARTER) {
return WhereClause::fromRaw($this->processLastQuarter($attribute));
}
if ($type === Type::CURRENT_YEAR) {
return WhereClause::fromRaw($this->processCurrentYear($attribute));
}
if ($type === Type::LAST_YEAR) {
return WhereClause::fromRaw($this->processLastYear($attribute));
}
if ($type === Type::CURRENT_FISCAL_YEAR) {
return WhereClause::fromRaw($this->processCurrentFiscalYear($attribute));
}
if ($type === Type::LAST_FISCAL_YEAR) {
return WhereClause::fromRaw($this->processLastFiscalYear($attribute));
}
if ($type === Type::CURRENT_FISCAL_QUARTER) {
return WhereClause::fromRaw($this->processCurrentFiscalQuarter($attribute));
}
if ($type === Type::LAST_FISCAL_QUARTER) {
return WhereClause::fromRaw($this->processLastFiscalQuarter($attribute));
}
if ($type === Type::BEFORE) {
return WhereClause::fromRaw($this->processBefore($attribute, $value));
}
if ($type === Type::AFTER) {
return WhereClause::fromRaw($this->processAfter($attribute, $value));
}
if ($type === Type::BETWEEN) {
return WhereClause::fromRaw($this->processBetween($attribute, $value));
}
if ($type === Type::LIKE) {
return WhereClause::fromRaw($this->processLike($attribute, $value));
}
if ($type === Type::NOT_LIKE) {
return WhereClause::fromRaw($this->processNotLike($attribute, $value));
}
if ($type === Type::IS_NULL) {
return WhereClause::fromRaw($this->processIsNull($attribute));
}
if ($type === Type::NONE) {
return WhereClause::fromRaw($this->processNone());
}
if ($type === Type::ANY) {
return WhereClause::fromRaw($this->processAny());
}
if ($type === Type::IS_NOT_NULL) {
return WhereClause::fromRaw($this->processIsNotNull($attribute));
}
if ($type === Type::IS_TRUE) {
return WhereClause::fromRaw($this->processIsTrue($attribute));
}
if ($type === Type::IS_FALSE) {
return WhereClause::fromRaw($this->processIsFalse($attribute));
}
if ($type === Type::STARTS_WITH) {
return WhereClause::fromRaw($this->processStartsWith($attribute, $value));
}
if ($type === Type::ENDS_WITH) {
return WhereClause::fromRaw($this->processEndsWith($attribute, $value));
}
if ($type === Type::CONTAINS) {
return WhereClause::fromRaw($this->processContains($attribute, $value));
}
if ($type === Type::NOT_CONTAINS) {
return WhereClause::fromRaw($this->processNotContains($attribute, $value));
}
if ($type === Type::GREATER_THAN) {
return WhereClause::fromRaw($this->processGreaterThan($attribute, $value));
}
if ($type === Type::LESS_THAN) {
return WhereClause::fromRaw($this->processLessThan($attribute, $value));
}
if ($type === Type::GREATER_THAN_OR_EQUALS) {
return WhereClause::fromRaw($this->processGreaterThanOrEquals($attribute, $value));
}
if ($type === Type::LESS_THAN_OR_EQUALS) {
return WhereClause::fromRaw($this->processLessThanOrEquals($attribute, $value));
}
if (!$this->itemConverterFactory->hasForType($type)) {
throw new BadRequest("Unknown where item type '$type'.");
}
$converter = $this->itemConverterFactory->createForType($type, $this->entityType, $this->user);
return $converter->convert($queryBuilder, $item);
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function groupProcessAndOr(SelectBuilder $queryBuilder, string $type, $value): array
{
if (!is_array($value)) {
throw new BadRequest("Bad where item.");
}
$whereClause = [];
foreach ($value as $item) {
$subPart = $this->convert($queryBuilder, Item::fromRaw($item))->getRaw();
foreach ($subPart as $left => $right) {
if (!empty($right) || is_null($right) || $right === '' || $right === 0 || $right === false) {
$whereClause[] = [
$left => $right,
];
}
}
}
return [
strtoupper($type) => $whereClause,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function groupProcessSubQuery(string $type, $value): array
{
if (!is_array($value)) {
throw new BadRequest("Bad where item.");
}
$sqQueryBuilder = SelectBuilder::create()
->from($this->entityType);
$whereItem = Item::fromRaw([
'type' => Type::AND,
'value' => $value,
]);
$whereClause = $this->convert($sqQueryBuilder, $whereItem)->getRaw();
$this->scanner->apply($sqQueryBuilder, $whereItem);
$rawParams = $sqQueryBuilder->build()->getRaw();
$key = $type === Type::SUBQUERY_IN ? 'id=s' : 'id!=s';
return [
$key => Select::fromRaw([
'select' => [Attribute::ID],
'from' => $this->entityType,
'whereClause' => $whereClause,
'leftJoins' => $rawParams['leftJoins'] ?? [],
'joins' => $rawParams['joins'] ?? [],
]),
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function groupProcessColumn(
string $type,
string $attribute,
mixed $value
): array {
$link = $this->metadata->get(['entityDefs', $this->entityType, 'fields', $attribute, 'link']);
$column = $this->metadata->get(['entityDefs', $this->entityType, 'fields', $attribute, 'column']);
if (!$column || !$link) {
throw new BadRequest("Bad where item 'column'.");
}
$subQueryBuilder = SelectBuilder::create()
->from($this->entityType)
->select(Attribute::ID)
->leftJoin($link);
$alias = $link;
$expr = "{$alias}Middle.$column";
if ($type === 'columnLike') {
$where = [$expr . '*' => $value];
} else if ($type === 'columnIn') {
$where = [$expr . '=' => $value];
} else if ($type === 'columnEquals') {
$where = [$expr . '=' => $value];
} else if ($type === 'columnNotEquals') {
$where = [$expr . '!=' => $value];
} else if ($type === 'columnNotIn') {
$where = [$expr . '!=' => $value];
} else if ($type === 'columnIsNull') {
$where = [$expr . '=' => null];
} else if ($type === 'columnIsNotNull') {
$where = [$expr . '!=' => null];
} else {
throw new BadRequest("Bad where item 'column'.");
}
return Cond::in(
Cond::column(Attribute::ID),
$subQueryBuilder
->where($where)
->build()
)->getRaw();
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function groupProcessArray(
SelectBuilder $queryBuilder,
string $type,
string $attribute,
$value
): array {
$arrayAttribute = $attribute;
$arrayEntityType = $this->entityType;
$idPart = Attribute::ID;
$isForeign = str_contains($attribute, '.');
$isForeignType = false;
$entityDefs = $this->ormDefs->getEntity($this->entityType);
if (!$isForeign) {
$isForeignType = $entityDefs->getAttribute($attribute)->getType() === Entity::FOREIGN;
$isForeign = $isForeignType;
}
if ($isForeign) {
if ($isForeignType) {
$arrayLink = $entityDefs->getAttribute($attribute)->getParam(AttributeParam::RELATION);
$arrayAttribute = $entityDefs->getAttribute($attribute)->getParam(AttributeParam::FOREIGN);
} else {
[$arrayLink, $arrayAttribute] = explode('.', $attribute);
}
if (!$arrayLink || !$arrayAttribute) {
throw new BadRequest("Bad where item.");
}
$arrayEntityType = $entityDefs->getRelation($arrayLink)->getForeignEntityType();
$arrayAlias = "{$arrayLink}ArrayFilter{$this->randomStringGenerator->generate()}";
$idPart = "$arrayAlias.id";
$queryBuilder->leftJoin($arrayLink, $arrayAlias);
// A sub-query is supposed to be used.
/*
$relationType = $entityDefs->getRelation($arrayLink)->getType();
if (
in_array($relationType, [
Entity::MANY_MANY,
Entity::HAS_MANY,
Entity::HAS_CHILDREN,
])
) {
$queryBuilder->distinct();
}
*/
}
if ($type === Type::ARRAY_ANY_OF) {
if (!$value && !is_array($value)) {
throw new BadRequest("Bad where item. No value.");
}
$subQuery = SelectBuilder::create()
->select('entityId')
->from(ArrayValue::ENTITY_TYPE)
->where([
'entityType' => $arrayEntityType,
'attribute' => $arrayAttribute,
'value' => $value,
])
->build();
return [$idPart . '=s' => $subQuery->getRaw()];
}
if ($type === Type::ARRAY_NONE_OF) {
if (!$value && !is_array($value)) {
throw new BadRequest("Bad where item 'array'. No value.");
}
return Cond::not(
Cond::exists(
SelectBuilder::create()
->select('entityId')
->from(ArrayValue::ENTITY_TYPE)
->where([
'entityType' => $arrayEntityType,
'attribute' => $arrayAttribute,
'value' => $value,
'entityId:' => lcfirst($arrayEntityType) . '.id'
])
->build()
)
)->getRaw();
}
if ($type === Type::ARRAY_IS_EMPTY) {
return Cond::not(
Cond::exists(
SelectBuilder::create()
->select('entityId')
->from(ArrayValue::ENTITY_TYPE)
->where([
'entityType' => $arrayEntityType,
'attribute' => $arrayAttribute,
'entityId:' => lcfirst($arrayEntityType) . '.id'
])
->build()
)
)->getRaw();
}
if ($type === Type::ARRAY_IS_NOT_EMPTY) {
$subQuery = SelectBuilder::create()
->select('entityId')
->from(ArrayValue::ENTITY_TYPE)
->where([
'entityType' => $arrayEntityType,
'attribute' => $arrayAttribute,
])
->build();
return [$idPart . '=s' => $subQuery];
}
if ($type === Type::ARRAY_ALL_OF) {
if (!$value && !is_array($value)) {
throw new BadRequest("Bad where item 'array'. No value.");
}
if (!is_array($value)) {
$value = [$value];
}
$whereList = [];
foreach ($value as $arrayValue) {
$whereList[] = [
$idPart .'=s' => SelectBuilder::create()
->from(ArrayValue::ENTITY_TYPE)
->select('entityId')
->where([
'value' => $arrayValue,
'attribute' => $arrayAttribute,
'entityType' => $arrayEntityType,
Attribute::DELETED => false,
])
->build()
];
}
return $whereList;
}
throw new BadRequest("Bad where item 'array'.");
}
/**
* A complex expression w/o a value.
*
* @return array<string|int, mixed>
*/
private function processExpression(string $attribute): array
{
$key = $attribute;
if (!str_ends_with($key, ':')) {
$key .= ':';
}
return [
$key => null,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processLike(string $attribute, $value): array
{
return [
$attribute . '*' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processNotLike(string $attribute, $value): array
{
return [
$attribute . '!*' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processEquals(string $attribute, $value): array
{
return [
$attribute . '=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processOn(string $attribute, $value): array
{
return $this->processEquals($attribute, $value);
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processNotEquals(string $attribute, $value): array
{
return [
$attribute . '!=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processNotOn(string $attribute, $value): array
{
return $this->processNotEquals($attribute, $value);
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processStartsWith(string $attribute, $value): array
{
return [
$attribute . '*' => $value . '%',
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processEndsWith(string $attribute, $value): array
{
return [
$attribute . '*' => '%' . $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processContains(string $attribute, $value): array
{
return [
$attribute . '*' => '%' . $value . '%',
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processNotContains(string $attribute, $value): array
{
return [
$attribute . '!*' => '%' . $value . '%',
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processGreaterThan(string $attribute, $value): array
{
return [
$attribute . '>' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processAfter(string $attribute, $value): array
{
return $this->processGreaterThan($attribute, $value);
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processLessThan(string $attribute, $value): array
{
return [
$attribute . '<' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processBefore(string $attribute, $value): array
{
return $this->processLessThan($attribute, $value);
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processGreaterThanOrEquals(string $attribute, $value): array
{
return [
$attribute . '>=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
*/
private function processLessThanOrEquals(string $attribute, $value): array
{
return [
$attribute . '<=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processIn(string $attribute, $value): array
{
if (!is_array($value)) {
throw new BadRequest("Bad where item 'in'.");
}
return [
$attribute . '=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processNotIn(string $attribute, $value): array
{
if (!is_array($value)) {
throw new BadRequest("Bad where item 'notIn'.");
}
return [
$attribute . '!=' => $value,
];
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processBetween(string $attribute, $value): array
{
if (!is_array($value) || count($value) < 2) {
throw new BadRequest("Bad where item 'between'.");
}
return [
'AND' => [
$attribute . '>=' => $value[0],
$attribute . '<=' => $value[1],
]
];
}
/**
* @return array<string, mixed>
*/
private function processAny(): array
{
return [
'true:' => null,
];
}
/**
* @return array<string, mixed>
*/
private function processNone(): array
{
return [
'false:' => null,
];
}
/**
* @return array<string|int, mixed>
*/
private function processIsNull(string $attribute): array
{
return [
$attribute . '=' => null,
];
}
/**
* @return array<string|int, mixed>
*/
private function processIsNotNull(string $attribute): array
{
return [
$attribute . '!=' => null,
];
}
/**
* @return array<string|int, mixed>
*/
private function processEver(string $attribute): array
{
return $this->processIsNotNull($attribute);
}
/**
* @return array<string|int, mixed>
*/
private function processIsTrue(string $attribute): array
{
return [
$attribute . '=' => true,
];
}
/**
* @return array<string|int, mixed>
*/
private function processIsFalse(string $attribute): array
{
return [
$attribute . '=' => false,
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processToday(string $attribute, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
return [
$attribute . '=' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processPast(string $attribute, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
return [
$attribute . '<' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processFuture(string $attribute, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
return [
$attribute . '>' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processLastSevenDays(string $attribute, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
$from = $today->addDays(-7);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<=' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processLastXDays(string $attribute, mixed $value, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
$number = intval($value);
$from = $today->addDays(- $number);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<=' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processNextXDays(string $attribute, mixed $value, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
$number = intval($value);
$to = $today->addDays($number);
return [
'AND' => [
$attribute . '>=' => $today->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<=' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processOlderThanXDays(string $attribute, mixed $value, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
$number = intval($value);
$date = $today->addDays(- $number);
return [
$attribute . '<' => $date->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
];
}
/**
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processAfterXDays(string $attribute, mixed $value, ?Data $data): array
{
$timeZone = $this->getTimeZone($data);
$today = DateTime::createNow()->withTimezone($timeZone);
$number = intval($value);
$date = $today->addDays($number);
return [
$attribute . '>' => $date->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
];
}
/**
* @return array<string|int, mixed>
*/
private function processCurrentMonth(string $attribute): array
{
$from = DateTime::createNow()
->withTimezone($this->getSystemTimeZone())
->modify('first day of this month');
$to = $from->addMonths(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processLastMonth( string $attribute): array
{
$from = DateTime::createNow()
->withTimezone($this->getSystemTimeZone())
->modify('first day of last month');
$to = $from->addMonths(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processNextMonth(string $attribute): array
{
$from = DateTime::createNow()
->withTimezone($this->getSystemTimeZone())
->modify('first day of next month');
$to = $from->addMonths(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processCurrentQuarter(string $attribute): array
{
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$quarter = intval(ceil($now->getMonth() / 3));
$from = $now
->modify('first day of January this year')
->addMonths(($quarter - 1) * 3);
$to = $from->addMonths(3);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processLastQuarter(string $attribute): array
{
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$quarter = intval(ceil($now->getMonth() / 3));
$from = $now->modify('first day of January this year');
$quarter--;
if ($quarter == 0) {
$quarter = 4;
$from = $from->addYears(-1);
}
$from = $from->addMonths(($quarter - 1) * 3);
$to = $from->addMonths(3);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processCurrentYear(string $attribute): array
{
$from = DateTime::createNow()
->withTimezone($this->getSystemTimeZone())
->modify('first day of January this year');
$to = $from->addYears(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processLastYear(string $attribute): array
{
$from = DateTime::createNow()
->withTimezone($this->getSystemTimeZone())
->modify('first day of January last year');
$to = $from->addYears(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processCurrentFiscalYear(string $attribute): array
{
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$from = $now
->modify('first day of January this year')
->addMonths($fiscalYearShift);
if ($now->getMonth() < $fiscalYearShift + 1) {
$from = $from->addYears(-1);
}
$to = $from->addYears(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processLastFiscalYear(string $attribute): array
{
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$from = $now
->modify('first day of January this year')
->addMonths($fiscalYearShift)
->addYears(-1);
if ($now->getMonth() < $fiscalYearShift + 1) {
$from = $from->addYears(-1);
}
$to = $from->addYears(1);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processCurrentFiscalQuarter(string $attribute): array
{
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$quarterShift = (int) floor(($now->getMonth() - $fiscalYearShift - 1) / 3);
$from = $now
->modify('first day of January this year')
->addMonths($fiscalYearShift)
->addMonths($quarterShift * 3);
$to = $from->addMonths(3);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processLastFiscalQuarter(string $attribute): array
{
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
$now = DateTime::createNow()
->withTimezone($this->getSystemTimeZone());
$quarterShift = (int) floor(($now->getMonth() - $fiscalYearShift - 1) / 3);
$from = $now
->modify('first day of January this year')
->addMonths($fiscalYearShift)
->addMonths($quarterShift * 3);
$from = $from->addMonths(-3);
$to = $from->addMonths(3);
return [
'AND' => [
$attribute . '>=' => $from->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
$attribute . '<' => $to->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_FORMAT),
]
];
}
/**
* @return array<string|int, mixed>
*/
private function processIsNotLinked(string $attribute): array
{
$link = $attribute;
$alias = $link . 'IsLinkedFilter' . $this->randomStringGenerator->generate();
$defs = $this->ormDefs->getEntity($this->entityType)->getRelation($link);
$relationType = $defs->getType();
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
$conditions = [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
];
foreach ($defs->getConditions() as $k => $v) {
$conditions["$alias.$k"] = $v;
}
// The foreign table is not joined as it would perform much slower.
// Trade off is that if a foreign record is deleted but the middle table
// is not yet deleted, it will give a non-actual result.
$subQuery = SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($middleEntityType, $alias, $conditions)
->where(["$alias.$key" => null])
->build();
return ['id=s' => $subQuery];
}
if (
$relationType == Entity::HAS_MANY ||
$relationType == Entity::HAS_ONE ||
$relationType == Entity::BELONGS_TO ||
$relationType === Entity::HAS_CHILDREN
) {
$subQuery = SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($link, $alias)
->where([$alias . '.id' => null])
->build();
return ['id=s' => $subQuery];
}
throw new RuntimeException("Bad where item. Not supported relation type.");
}
/**
* @return array<string|int, mixed>
*/
private function processIsLinked(string $attribute): array
{
$link = $attribute;
$alias = $link . 'IsLinkedFilter' . $this->randomStringGenerator->generate();
$defs = $this->ormDefs->getEntity($this->entityType)->getRelation($link);
$relationType = $defs->getType();
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
$conditions = [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
];
foreach ($defs->getConditions() as $k => $v) {
$conditions["$alias.$k"] = $v;
}
// The foreign table is not joined as it would perform much slower.
// Trade off is that if a foreign record is deleted but the middle table
// is not yet deleted, it will give a non-actual result.
$subQuery = SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($middleEntityType, $alias, $conditions)
->where(["$alias.$key!=" => null])
->build();
return ['id=s' => $subQuery];
}
if (
$relationType == Entity::HAS_MANY ||
$relationType == Entity::HAS_ONE ||
$relationType == Entity::BELONGS_TO ||
$relationType == Entity::HAS_CHILDREN
) {
$subQuery = SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($link, $alias)
->where([$alias . '.id!=' => null])
->build();
return ['id=s' => $subQuery];
}
throw new RuntimeException("Bad where item. Not supported relation type.");
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processLinkedWith(string $attribute, $value): array
{
$link = $attribute;
if (!$this->ormDefs->getEntity($this->entityType)->hasRelation($link)) {
throw new BadRequest("Not existing link '$link' in where item.");
}
$defs = $this->ormDefs->getEntity($this->entityType)->getRelation($link);
$alias = $link . 'LinkedWithFilter' . $this->randomStringGenerator->generate();
if (!is_array($value)) {
$value = [$value];
}
foreach ($value as $it) {
if (!is_string($it) && !is_int($it)) {
throw new BadRequest("Bad where item. Bad array item.");
}
}
$relationType = $defs->getType();
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
// IN-sub-query performs faster than EXISTS on MariaDB when multiple IDs.
// Left-join performs faster than inner-join.
// Not joining a foreign table as it affects performance in MySQL.
// MariaDB and PostgreSQL perform fast, MySQL slow.
return Cond::in(
Cond::column(Attribute::ID),
SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin(
Join::create($link, $alias)
->withOnlyMiddle()
)
->where(["$alias.$key" => $value])
->build()
)->getRaw();
}
if (
$relationType == Entity::HAS_MANY ||
$relationType == Entity::HAS_ONE
) {
$subQuery = SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($link, $alias)
->where([$alias . '.id' => $value])
->build();
return ['id=s' => $subQuery];
}
if ($relationType == Entity::BELONGS_TO) {
$key = $defs->getKey();
return [$key => $value];
}
throw new BadRequest("Bad where item. Not supported relation type.");
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processNotLinkedWith(string $attribute, $value): array
{
$link = $attribute;
if (!$this->ormDefs->getEntity($this->entityType)->hasRelation($link)) {
throw new BadRequest("Not existing link '$link' in where item.");
}
$defs = $this->ormDefs->getEntity($this->entityType)->getRelation($link);
$alias = $link . 'NotLinkedWithFilter' . $this->randomStringGenerator->generate();
if (!is_array($value)) {
$value = [$value];
}
foreach ($value as $it) {
if (!is_string($it) && !is_int($it)) {
throw new BadRequest("Bad where item. Bad array item.");
}
}
$relationType = $defs->getType();
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
// MariaDB and MySQL perform slow, PostgreSQL fast.
return Cond::not(
Cond::exists(
SelectBuilder::create()
->from($this->entityType, 'sq')
->join(
Join::create($link, $alias)
->withOnlyMiddle()
)
->where(["$alias.$key" => $value])
->where(
Cond::equal(
Cond::column('sq.id'),
Cond::column(lcfirst($this->entityType) . '.id')
)
)
->build()
)
)->getRaw();
}
if (
$relationType == Entity::HAS_MANY ||
$relationType == Entity::HAS_ONE
) {
return Cond::not(
Cond::exists(
SelectBuilder::create()
->select(Attribute::ID)
->from($this->entityType, 'sq')
->join($link, $alias)
->where(["$alias.id" => $value])
->where(['sq.id:' => lcfirst($this->entityType) . '.id'])
->build()
)
)->getRaw();
}
if ($relationType == Entity::BELONGS_TO) {
$key = $defs->getKey();
return [$key . '!=' => $value];
}
throw new BadRequest("Bad where item. Not supported relation type.");
}
/**
* @param mixed $value
* @return array<string|int, mixed>
* @throws BadRequest
*/
private function processLinkedWithAll(string $attribute, $value): array
{
$link = $attribute;
if (!$this->ormDefs->getEntity($this->entityType)->hasRelation($link)) {
throw new BadRequest("Not existing link '$link' in where item.");
}
if (!$value && !is_array($value)) {
throw new BadRequest("Bad where item. Empty value.");
}
if (!is_array($value)) {
$value = [$value];
}
foreach ($value as $it) {
if (!is_string($it) && !is_int($it)) {
throw new BadRequest("Bad where item. Bad array item.");
}
}
$defs = $this->ormDefs->getEntity($this->entityType)->getRelation($link);
$relationType = $defs->getType();
if ($relationType === Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$whereList = [];
foreach ($value as $targetId) {
// Only-middle join performs slower on MariaDB.
$sq = SelectBuilder::create()
->from($this->entityType)
->select(Attribute::ID)
->leftJoin($link)
->where([
$link . 'Middle.' . $key => $targetId,
])
->build();
$whereList[] = [Attribute::ID . '=s' => $sq];
}
return $whereList;
}
if ($relationType === Entity::HAS_MANY) {
$whereList = [];
foreach ($value as $targetId) {
$sq = SelectBuilder::create()
->from($this->entityType)
->select(Attribute::ID)
->leftJoin($link)
->where([$link . '.id' => $targetId])
->build();
$whereList[] = [Attribute::ID . '=s' => $sq];
}
return $whereList;
}
throw new BadRequest("Bad where item. Not supported relation type.");
}
private function getSystemTimeZone(): DateTimeZone
{
$timeZone = $this->applicationConfig->getTimeZone();
try {
return new DateTimeZone($timeZone);
} catch (Exception $e) {
throw new RuntimeException($e->getMessage());
}
}
/**
* @throws BadRequest
*/
private function getTimeZone(?Data $data): DateTimeZone
{
$timeZone = $data instanceof Data\Date ? $data->getTimeZone() : null;
if (!$timeZone) {
return $this->getSystemTimeZone();
}
try {
return new DateTimeZone($timeZone);
} catch (Exception $e) {
throw new BadRequest($e->getMessage());
}
}
}