Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
<?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\Utils\Database\Orm\Defs;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Type\AttributeType;
/**
* Immutable.
*/
class AttributeDefs
{
/** @var array<string, mixed> */
private array $params = [];
private function __construct(private string $name) {}
public static function create(string $name): self
{
return new self($name);
}
/**
* Get an attribute name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Get a type.
*
* @return AttributeType::*
*/
public function getType(): ?string
{
/** @var ?AttributeType::* $value */
$value = $this->getParam(AttributeParam::TYPE);
return $value;
}
/**
* Clone with a type.
*
* @param AttributeType::* $type
*/
public function withType(string $type): self
{
return $this->withParam(AttributeParam::TYPE, $type);
}
/**
* Clone with a DB type.
*/
public function withDbType(string $dbType): self
{
return $this->withParam(AttributeParam::DB_TYPE, $dbType);
}
/**
* Clone with not-storable.
*/
public function withNotStorable(bool $value = true): self
{
return $this->withParam(AttributeParam::NOT_STORABLE, $value);
}
/**
* Clone with a length.
*/
public function withLength(int $length): self
{
return $this->withParam(AttributeParam::LEN, $length);
}
/**
* Clone with a default value.
*/
public function withDefault(mixed $value): self
{
return $this->withParam(AttributeParam::DEFAULT, $value);
}
/**
* Whether a parameter is set.
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->params);
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* Clone with a parameter.
*/
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* Clone without a parameter.
*/
public function withoutParam(string $name): self
{
$obj = clone $this;
unset($obj->params[$name]);
return $obj;
}
/**
* Clone with parameters merged.
*
* @param array<string, mixed> $params
*/
public function withParamsMerged(array $params): self
{
$obj = clone $this;
/** @var array<string, mixed> $params */
$params = Util::merge($this->params, $params);
$obj->params = $params;
return $obj;
}
/**
* To an associative array.
*
* @return array<string, mixed>
*/
public function toAssoc(): array
{
return $this->params;
}
}

View File

@@ -0,0 +1,155 @@
<?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\Utils\Database\Orm\Defs;
use Espo\ORM\Defs\Params\EntityParam;
/**
* Immutable.
*/
class EntityDefs
{
/** @var array<string, AttributeDefs> */
private array $attributes = [];
/** @var array<string, RelationDefs> */
private array $relations = [];
/** @var array<string, IndexDefs> */
private array $indexes = [];
private function __construct() {}
public static function create(): self
{
return new self();
}
public function withAttribute(AttributeDefs $attributeDefs): self
{
$obj = clone $this;
$obj->attributes[$attributeDefs->getName()] = $attributeDefs;
return $obj;
}
public function withRelation(RelationDefs $relationDefs): self
{
$obj = clone $this;
$obj->relations[$relationDefs->getName()] = $relationDefs;
return $obj;
}
public function withIndex(IndexDefs $index): self
{
$obj = clone $this;
$obj->indexes[$index->getName()] = $index;
return $obj;
}
public function withoutAttribute(string $name): self
{
$obj = clone $this;
unset($obj->attributes[$name]);
return $obj;
}
public function withoutRelation(string $name): self
{
$obj = clone $this;
unset($obj->relations[$name]);
return $obj;
}
public function withoutIndex(string $name): self
{
$obj = clone $this;
unset($obj->indexes[$name]);
return $obj;
}
public function getAttribute(string $name): ?AttributeDefs
{
return $this->attributes[$name] ?? null;
}
public function getRelation(string $name): ?RelationDefs
{
return $this->relations[$name] ?? null;
}
public function getIndex(string $name): ?IndexDefs
{
return $this->indexes[$name] ?? null;
}
/**
* @return array<string, array<string, mixed>>
*/
public function toAssoc(): array
{
$data = [];
if (count($this->attributes)) {
$attributesData = [];
foreach ($this->attributes as $name => $attributeDefs) {
$attributesData[$name] = $attributeDefs->toAssoc();
}
$data[EntityParam::ATTRIBUTES] = $attributesData;
}
if (count($this->relations)) {
$relationsData = [];
foreach ($this->relations as $name => $relationDefs) {
$relationsData[$name] = $relationDefs->toAssoc();
}
$data[EntityParam::RELATIONS] = $relationsData;
}
if (count($this->indexes)) {
$indexesData = [];
foreach ($this->indexes as $name => $indexDefs) {
$indexesData[$name] = $indexDefs->toAssoc();
}
$data[EntityParam::INDEXES] = $indexesData;
}
return $data;
}
}

View File

@@ -0,0 +1,175 @@
<?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\Utils\Database\Orm\Defs;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\Params\IndexParam;
/**
* Immutable.
*/
class IndexDefs
{
/** @var array<string, mixed> */
private array $params = [];
private function __construct(private string $name) {}
public static function create(string $name): self
{
return new self($name);
}
/**
* Get a relation name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Whether a parameter is set.
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->params);
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* Clone with a parameter.
*/
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* Clone without a parameter.
*/
public function withoutParam(string $name): self
{
$obj = clone $this;
unset($obj->params[$name]);
return $obj;
}
public function withUnique(): self
{
$obj = clone $this;
$obj->params[IndexParam::TYPE] = 'unique';
return $obj;
}
public function withoutUnique(): self
{
$obj = clone $this;
unset($obj->params[IndexParam::TYPE]);
return $obj;
}
public function withFlag(string $flag): self
{
$obj = clone $this;
$flags = $obj->params[IndexParam::FLAGS] ?? [];
if (!in_array($flag, $flags)) {
$flags[] = $flag;
}
$obj->params[IndexParam::FLAGS] = $flags;
return $obj;
}
public function withoutFlag(string $flag): self
{
$obj = clone $this;
$flags = $obj->params[IndexParam::FLAGS] ?? [];
$index = array_search($flag, $flags, true);
if ($index !== -1) {
unset($flags[$index]);
$flags = array_values($flags);
}
$obj->params[IndexParam::FLAGS] = $flags;
if ($flags === []) {
unset($obj->params[IndexParam::FLAGS]);
}
return $obj;
}
/**
* Clone with parameters merged.
*
* @param array<string, mixed> $params
*/
public function withParamsMerged(array $params): self
{
$obj = clone $this;
/** @var array<string, mixed> $params */
$params = Util::merge($this->params, $params);
$obj->params = $params;
return $obj;
}
/**
* To an associative array.
*
* @return array<string, mixed>
*/
public function toAssoc(): array
{
return $this->params;
}
}

View File

@@ -0,0 +1,256 @@
<?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\Utils\Database\Orm\Defs;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Type\RelationType;
class RelationDefs
{
/** @var array<string, mixed> */
private array $params = [];
private function __construct(private string $name) {}
public static function create(string $name): self
{
return new self($name);
}
/**
* Get a relation name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Get a type.
*
* @return RelationType::*
*/
public function getType(): ?string
{
/** @var ?RelationType::* */
return $this->getParam(RelationParam::TYPE);
}
/**
* Clone with a type.
*
* @param RelationType::* $type
*/
public function withType(string $type): self
{
return $this->withParam(RelationParam::TYPE, $type);
}
/**
* Clone with a foreign entity type.
*/
public function withForeignEntityType(string $entityType): self
{
return $this->withParam(RelationParam::ENTITY, $entityType);
}
/**
* Get a foreign entity type.
*/
public function getForeignEntityType(): ?string
{
return $this->getParam(RelationParam::ENTITY);
}
/**
* Clone with a foreign relation name.
*/
public function withForeignRelationName(?string $name): self
{
return $this->withParam(RelationParam::FOREIGN, $name);
}
/**
* Get a foreign relation name.
*/
public function getForeignRelationName(): ?string
{
return $this->getParam(RelationParam::FOREIGN);
}
/**
* Clone with a relationship name.
*/
public function withRelationshipName(string $name): self
{
return $this->withParam(RelationParam::RELATION_NAME, $name);
}
/**
* Get a foreign relation name.
*/
public function getRelationshipName(): ?string
{
return $this->getParam(RelationParam::RELATION_NAME);
}
/**
* Clone with a key.
*/
public function withKey(string $key): self
{
return $this->withParam(RelationParam::KEY, $key);
}
/**
* Get a key.
*/
public function getKey(): ?string
{
return $this->getParam(RelationParam::KEY);
}
/**
* Clone with a key.
*/
public function withForeignKey(string $foreignKey): self
{
return $this->withParam(RelationParam::FOREIGN_KEY, $foreignKey);
}
/**
* Get a key.
*/
public function getForeignKey(): ?string
{
return $this->getParam(RelationParam::FOREIGN_KEY);
}
/**
* Clone with middle keys.
*/
public function withMidKeys(string $midKey, string $foreignMidKey): self
{
return $this->withParam(RelationParam::MID_KEYS, [$midKey, $foreignMidKey]);
}
/**
* Whether a parameter is set.
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->params);
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* Clone with a parameter.
*/
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* Clone without a parameter.
*/
public function withoutParam(string $name): self
{
$obj = clone $this;
unset($obj->params[$name]);
return $obj;
}
/**
* Clone with conditions. Conditions are used for relationships that share a same middle table.
*
* @param array<string, scalar|(array<int, mixed>)|null> $conditions
*/
public function withConditions(array $conditions): self
{
$obj = clone $this;
return $obj->withParam(RelationParam::CONDITIONS, $conditions);
}
/**
* Clone with an additional middle table column.
*/
public function withAdditionalColumn(AttributeDefs $attributeDefs): self
{
$obj = clone $this;
/** @var array<string, array<string, mixed>> $list */
$list = $obj->getParam(RelationParam::ADDITIONAL_COLUMNS) ?? [];
$list[$attributeDefs->getName()] = $attributeDefs->toAssoc();
return $obj->withParam(RelationParam::ADDITIONAL_COLUMNS, $list);
}
/**
* Clone with parameters merged.
*
* @param array<string, mixed> $params
*/
public function withParamsMerged(array $params): self
{
$obj = clone $this;
/** @var array<string, mixed> $params */
$params = Util::merge($this->params, $params);
$obj->params = $params;
return $obj;
}
/**
* To an associative array.
*
* @return array<string, mixed>
*/
public function toAssoc(): array
{
return $this->params;
}
}

View File

@@ -0,0 +1,41 @@
<?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\Utils\Database\Orm;
use Espo\ORM\Defs\FieldDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
/**
* Converts field definitions to ORM definitions.
*/
interface FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs;
}

View File

@@ -0,0 +1,70 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Type\AttributeType;
class AttachmentMultiple implements FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParamsMerged([
'orderBy' => [
[Field::CREATED_AT, Order::ASC],
[Field::NAME, Order::ASC],
],
AttributeParam::IS_LINK_MULTIPLE_ID_LIST => true,
'relation' => $name,
])
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParamsMerged([
AttributeParam::IS_LINK_MULTIPLE_NAME_MAP => true,
])
);
}
}

View File

@@ -0,0 +1,350 @@
<?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\Utils\Database\Orm\FieldConverters;
use Doctrine\DBAL\Types\Types;
use Espo\Core\Currency\ConfigDataProvider;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\ORM\Query\Part\Expression as Expr;
use Espo\ORM\Type\AttributeType;
class Currency implements FieldConverter
{
private const DEFAULT_PRECISION = 13;
private const DEFAULT_SCALE = 4;
public function __construct(
private Config $config,
private ConfigDataProvider $configDataProvider
) {}
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$amountDefs = AttributeDefs::create($name)
->withType(AttributeType::FLOAT)
->withParamsMerged([
'attributeRole' => 'value',
'fieldType' => FieldType::CURRENCY,
]);
$currencyDefs = AttributeDefs::create($name . 'Currency')
->withType(AttributeType::VARCHAR)
->withParamsMerged([
'attributeRole' => 'currency',
'fieldType' => FieldType::CURRENCY,
]);
$convertedDefs = null;
if ($fieldDefs->getParam(FieldParam::DECIMAL)) {
$dbType = $fieldDefs->getParam(FieldParam::DB_TYPE) ?? Types::DECIMAL;
$precision = $fieldDefs->getParam(FieldParam::PRECISION) ?? self::DEFAULT_PRECISION;
$scale = $fieldDefs->getParam(FieldParam::SCALE) ?? self::DEFAULT_SCALE;
$amountDefs = $amountDefs
->withType(AttributeType::VARCHAR)
->withDbType($dbType)
->withParam(AttributeParam::PRECISION, $precision)
->withParam(AttributeParam::SCALE, $scale);
$defaultValue = $fieldDefs->getParam(AttributeParam::DEFAULT);
if (is_int($defaultValue) || is_float($defaultValue)) {
$defaultValue = number_format($defaultValue, $scale, '.', '');
$amountDefs = $amountDefs->withParam(AttributeParam::DEFAULT, $defaultValue);
}
}
if ($fieldDefs->isNotStorable()) {
$amountDefs = $amountDefs->withNotStorable();
$currencyDefs = $currencyDefs->withNotStorable();
}
if (!$fieldDefs->isNotStorable()) {
[$amountDefs, $convertedDefs] = $this->config->get('currencyNoJoinMode') ?
$this->applyNoJoinMode($fieldDefs, $amountDefs) :
$this->applyJoinMode($fieldDefs, $amountDefs, $entityType);
}
$entityDefs = EntityDefs::create()
->withAttribute($amountDefs)
->withAttribute($currencyDefs);
if ($convertedDefs) {
$entityDefs = $entityDefs->withAttribute($convertedDefs);
}
return $entityDefs;
}
/**
* @return array{AttributeDefs, AttributeDefs}
*/
private function applyNoJoinMode(FieldDefs $fieldDefs, AttributeDefs $amountDefs): array
{
$name = $fieldDefs->getName();
$currencyAttribute = $name . 'Currency';
$defaultCurrency = $this->configDataProvider->getDefaultCurrency();
$baseCurrency = $this->configDataProvider->getBaseCurrency();
$rates = $this->configDataProvider->getCurrencyRates()->toAssoc();
if ($defaultCurrency !== $baseCurrency) {
$rates = $this->exchangeRates($baseCurrency, $defaultCurrency, $rates);
}
$expr = Expr::multiply(
Expr::column($name),
Expr::if(
Expr::equal(Expr::column($currencyAttribute), $defaultCurrency),
1.0,
$this->buildExpression($currencyAttribute, $rates)
)
)->getValue();
$exprForeign = Expr::multiply(
Expr::column("ALIAS.{$name}"),
Expr::if(
Expr::equal(Expr::column("ALIAS.{$name}Currency"), $defaultCurrency),
1.0,
$this->buildExpression("ALIAS.{$name}Currency", $rates)
)
)->getValue();
$exprForeign = str_replace('ALIAS', '{alias}', $exprForeign);
$convertedDefs = AttributeDefs::create($name . 'Converted')
->withType(AttributeType::FLOAT)
->withParamsMerged([
'select' => [
'select' => $expr,
],
'selectForeign' => [
'select' => $exprForeign,
],
'where' => [
"=" => [
'whereClause' => [
$expr . '=' => '{value}',
],
],
">" => [
'whereClause' => [
$expr . '>' => '{value}',
],
],
"<" => [
'whereClause' => [
$expr . '<' => '{value}',
],
],
">=" => [
'whereClause' => [
$expr . '>=' => '{value}',
],
],
"<=" => [
'whereClause' => [
$expr . '<=' => '{value}',
],
],
"<>" => [
'whereClause' => [
$expr . '!=' => '{value}',
],
],
"IS NULL" => [
'whereClause' => [
$expr . '=' => null,
],
],
"IS NOT NULL" => [
'whereClause' => [
$expr . '!=' => null,
],
],
],
AttributeParam::NOT_STORABLE => true,
'order' => [
'order' => [
[$expr, '{direction}'],
],
],
'attributeRole' => 'valueConverted',
'fieldType' => FieldType::CURRENCY,
]);
return [$amountDefs, $convertedDefs];
}
/**
* @param array<string, float> $currencyRates
* @return array<string, float>
*/
private function exchangeRates(string $baseCurrency, string $defaultCurrency, array $currencyRates): array
{
$precision = 5;
$defaultCurrencyRate = round(1 / $currencyRates[$defaultCurrency], $precision);
$exchangedRates = [];
$exchangedRates[$baseCurrency] = $defaultCurrencyRate;
unset($currencyRates[$baseCurrency], $currencyRates[$defaultCurrency]);
foreach ($currencyRates as $currencyName => $rate) {
$exchangedRates[$currencyName] = round($rate * $defaultCurrencyRate, $precision);
}
return $exchangedRates;
}
/**
* @param array<string, float> $rates
*/
private function buildExpression(string $currencyAttribute, array $rates): Expr|float
{
if ($rates === []) {
return 0.0;
}
$currency = array_key_first($rates);
$value = $rates[$currency];
unset($rates[$currency]);
return Expr::if(
Expr::equal(Expr::column($currencyAttribute), $currency),
$value,
$this->buildExpression($currencyAttribute, $rates)
);
}
/**
* @return array{AttributeDefs, AttributeDefs}
*/
private function applyJoinMode(FieldDefs $fieldDefs, AttributeDefs $amountDefs, string $entityType): array
{
$name = $fieldDefs->getName();
$alias = $name . 'CurrencyRate';
$leftJoins = [
[
'Currency',
$alias,
[$alias . '.id:' => $name . 'Currency'],
]
];
$foreignCurrencyAlias = "{$alias}{$entityType}{alias}Foreign";
$mulExpression = "MUL:({$name}, {$alias}.rate)";
$amountDefs = $amountDefs->withParamsMerged([
'order' => [
'order' => [
[$mulExpression, '{direction}'],
],
'leftJoins' => $leftJoins,
'additionalSelect' => ["{$alias}.rate"],
]
]);
$convertedDefs = AttributeDefs::create($name . 'Converted')
->withType(AttributeType::FLOAT)
->withParamsMerged([
'select' => [
'select' => $mulExpression,
'leftJoins' => $leftJoins,
],
'selectForeign' => [
'select' => "MUL:({alias}.{$name}, {$foreignCurrencyAlias}.rate)",
'leftJoins' => [
[
'Currency',
$foreignCurrencyAlias,
[$foreignCurrencyAlias . '.id:' => "{alias}.{$name}Currency"]
]
],
],
'where' => [
"=" => [
'whereClause' => [$mulExpression . '=' => '{value}'],
'leftJoins' => $leftJoins,
],
">" => [
'whereClause' => [$mulExpression . '>' => '{value}'],
'leftJoins' => $leftJoins,
],
"<" => [
'whereClause' => [$mulExpression . '<' => '{value}'],
'leftJoins' => $leftJoins,
],
">=" => [
'whereClause' => [$mulExpression . '>=' => '{value}'],
'leftJoins' => $leftJoins,
],
"<=" => [
'whereClause' => [$mulExpression . '<=' => '{value}'],
'leftJoins' => $leftJoins,
],
"<>" => [
'whereClause' => [$mulExpression . '!=' => '{value}'],
'leftJoins' => $leftJoins,
],
"IS NULL" => [
'whereClause' => [$name . '=' => null],
],
"IS NOT NULL" => [
'whereClause' => [$name . '!=' => null],
],
],
AttributeParam::NOT_STORABLE => true,
'order' => [
'order' => [
[$mulExpression, '{direction}'],
],
'leftJoins' => $leftJoins,
'additionalSelect' => ["{$alias}.rate"],
],
'attributeRole' => 'valueConverted',
'fieldType' => FieldType::CURRENCY,
]);
return [$amountDefs, $convertedDefs];
}
}

View File

@@ -0,0 +1,423 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\Entities\EmailAddress;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class Email implements FieldConverter
{
private const COLUMN_ENTITY_TYPE_LENGTH = 100;
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$foreignJoinAlias = "$name$entityType{alias}Foreign";
$foreignJoinMiddleAlias = "$name$entityType{alias}ForeignMiddle";
$emailAddressDefs = AttributeDefs
::create($name)
->withType(AttributeType::VARCHAR)
->withParamsMerged(
$this->getEmailAddressParams($entityType, $foreignJoinAlias, $foreignJoinMiddleAlias)
);
$dataDefs = AttributeDefs
::create($name . 'Data')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParamsMerged([
AttributeParam::NOT_EXPORTABLE => true,
'isEmailAddressData' => true,
'field' => $name,
]);
$isOptedOutDefs = AttributeDefs
::create($name . 'IsOptedOut')
->withType(AttributeType::BOOL)
->withNotStorable()
->withParamsMerged(
$this->getIsOptedOutParams($foreignJoinAlias, $foreignJoinMiddleAlias)
);
$isInvalidDefs = AttributeDefs
::create($name . 'IsInvalid')
->withType(AttributeType::BOOL)
->withNotStorable()
->withParamsMerged(
$this->getIsInvalidParams($foreignJoinAlias, $foreignJoinMiddleAlias)
);
$relationDefs = RelationDefs
::create('emailAddresses')
->withType(RelationType::MANY_MANY)
->withForeignEntityType(EmailAddress::ENTITY_TYPE)
->withRelationshipName('entityEmailAddress')
->withMidKeys('entityId', 'emailAddressId')
->withConditions(['entityType' => $entityType])
->withAdditionalColumn(
AttributeDefs
::create('entityType')
->withType(AttributeType::VARCHAR)
->withLength(self::COLUMN_ENTITY_TYPE_LENGTH)
)
->withAdditionalColumn(
AttributeDefs
::create('primary')
->withType(AttributeType::BOOL)
->withDefault(false)
);
return EntityDefs::create()
->withAttribute($emailAddressDefs)
->withAttribute($dataDefs)
->withAttribute($isOptedOutDefs)
->withAttribute($isInvalidDefs)
->withRelation($relationDefs);
}
/**
* @return array<string, mixed>
*/
private function getEmailAddressParams(
string $entityType,
string $foreignJoinAlias,
string $foreignJoinMiddleAlias,
): array {
return [
'select' => [
"select" => "emailAddresses.name",
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
],
'selectForeign' => [
"select" => "$foreignJoinAlias.name",
'leftJoins' => [
[
'EntityEmailAddress',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
EmailAddress::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.emailAddressId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'fieldType' => FieldType::EMAIL,
'where' => [
'LIKE' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'joins' => [
[
'emailAddress',
'emailAddress',
[
'emailAddress.id:' => 'emailAddressId',
'emailAddress.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
"LIKE:(emailAddress.lower, LOWER:({value})):" => null,
],
],
],
],
'NOT LIKE' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'joins' => [
[
'emailAddress',
'emailAddress',
[
'emailAddress.id:' => 'emailAddressId',
'emailAddress.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
"LIKE:(emailAddress.lower, LOWER:({value})):" => null,
],
],
],
],
'=' => [
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'whereClause' => [
"EQUAL:(emailAddressesMultiple.lower, LOWER:({value})):" => null,
]
],
'<>' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'joins' => [
[
'emailAddress',
'emailAddress',
[
'emailAddress.id:' => 'emailAddressId',
'emailAddress.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
"EQUAL:(emailAddress.lower, LOWER:({value})):" => null,
],
],
],
],
'IN' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'joins' => [
[
'emailAddress',
'emailAddress',
[
'emailAddress.id:' => 'emailAddressId',
'emailAddress.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
"emailAddress.lower" => '{value}',
],
],
],
],
'NOT IN' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'joins' => [
[
'emailAddress',
'emailAddress',
[
'emailAddress.id:' => 'emailAddressId',
'emailAddress.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
"emailAddress.lower" => '{value}',
],
],
],
],
'IS NULL' => [
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'whereClause' => [
'emailAddressesMultiple.lower=' => null,
]
],
'IS NOT NULL' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityEmailAddress',
'select' => ['entityId'],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
],
],
],
],
],
'order' => [
'order' => [
['emailAddresses.lower', '{direction}'],
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
'additionalSelect' => ['emailAddresses.lower'],
],
];
}
/**
* @return array<string, mixed>
*/
private function getIsOptedOutParams(string $foreignJoinAlias, string $foreignJoinMiddleAlias): array
{
return [
'select' => [
'select' => "emailAddresses.optOut",
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
],
'selectForeign' => [
'select' => "$foreignJoinAlias.optOut",
'leftJoins' => [
[
'EntityEmailAddress',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
EmailAddress::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.emailAddressId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'where' => [
'= TRUE' => [
'whereClause' => [
['emailAddresses.optOut=' => true],
['emailAddresses.optOut!=' => null],
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
],
'= FALSE' => [
'whereClause' => [
'OR' => [
['emailAddresses.optOut=' => false],
['emailAddresses.optOut=' => null],
]
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
]
],
'order' => [
'order' => [
['emailAddresses.optOut', '{direction}'],
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
'additionalSelect' => ['emailAddresses.optOut'],
],
];
}
/**
* @return array<string, mixed>
*/
private function getIsInvalidParams(string $foreignJoinAlias, string $foreignJoinMiddleAlias): array
{
return [
'select' => [
'select' => "emailAddresses.invalid",
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
],
'selectForeign' => [
'select' => "$foreignJoinAlias.invalid",
'leftJoins' => [
[
'EntityEmailAddress',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
EmailAddress::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.emailAddressId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'where' => [
'= TRUE' => [
'whereClause' => [
['emailAddresses.invalid=' => true],
['emailAddresses.invalid!=' => null],
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
],
'= FALSE' => [
'whereClause' => [
'OR' => [
['emailAddresses.invalid=' => false],
['emailAddresses.invalid=' => null],
]
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
]
],
'order' => [
'order' => [
['emailAddresses.invalid', '{direction}'],
],
'leftJoins' => [['emailAddresses', 'emailAddresses', ['primary' => true]]],
'additionalSelect' => ['emailAddresses.invalid'],
],
];
}
}

View File

@@ -0,0 +1,100 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\Name\Field;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\Entities\Attachment;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class File implements FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$idName = $name . 'Id';
$nameName = $name . 'Name';
$idDefs = AttributeDefs::create($idName)
->withType(AttributeType::FOREIGN_ID)
->withParam('index', false);
$nameDefs = AttributeDefs::create($nameName)
->withType(AttributeType::FOREIGN);
if ($fieldDefs->isNotStorable()) {
$idDefs = $idDefs->withNotStorable();
$nameDefs = $nameDefs->withType(AttributeType::VARCHAR);
}
/** @var array<string, mixed> $defaults */
$defaults = $fieldDefs->getParam('defaultAttributes') ?? [];
if (array_key_exists($idName, $defaults)) {
$idDefs = $idDefs->withDefault($defaults[$idName]);
}
$relationDefs = null;
if (!$fieldDefs->isNotStorable()) {
$nameDefs = $nameDefs->withParamsMerged([
AttributeParam::RELATION => $name,
AttributeParam::FOREIGN => Field::NAME,
]);
$relationDefs = RelationDefs::create($name)
->withType(RelationType::BELONGS_TO)
->withForeignEntityType(Attachment::ENTITY_TYPE)
->withKey($idName)
->withForeignKey(Attribute::ID)
->withParam(RelationParam::FOREIGN, null);
}
$entityDefs = EntityDefs::create()
->withAttribute($idDefs)
->withAttribute($nameDefs);
if ($relationDefs) {
$entityDefs = $entityDefs->withRelation($relationDefs);
}
return $entityDefs;
}
}

View File

@@ -0,0 +1,79 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Type\AttributeType;
class Link implements FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$idName = $name . 'Id';
$nameName = $name . 'Name';
$idDefs = AttributeDefs::create($idName)
->withType(AttributeType::FOREIGN_ID)
->withParamsMerged([
'index' => $name,
'attributeRole' => 'id',
'fieldType' => FieldType::LINK,
]);
$nameDefs = AttributeDefs::create($nameName)
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged([
'attributeRole' => 'name',
'fieldType' => FieldType::LINK,
]);
if ($fieldDefs->isNotStorable()) {
$idDefs = $idDefs->withNotStorable();
}
/** @var array<string, mixed> $defaults */
$defaults = $fieldDefs->getParam('defaultAttributes') ?? [];
if (array_key_exists($idName, $defaults)) {
$idDefs = $idDefs->withDefault($defaults[$idName]);
}
return EntityDefs::create()
->withAttribute($idDefs)
->withAttribute($nameDefs);
}
}

View File

@@ -0,0 +1,110 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Type\AttributeType;
class LinkMultiple implements FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$idsName = $name . 'Ids';
$namesName = $name . 'Names';
$columnsName = $name . 'Columns';
$idsDefs = AttributeDefs::create($idsName)
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParamsMerged([
AttributeParam::IS_LINK_MULTIPLE_ID_LIST => true,
'relation' => $name,
'isUnordered' => true,
'attributeRole' => 'idList',
'fieldType' => FieldType::LINK_MULTIPLE,
]);
/** @var array<string, mixed> $defaults */
$defaults = $fieldDefs->getParam('defaultAttributes') ?? [];
if (array_key_exists($idsName, $defaults)) {
$idsDefs = $idsDefs->withDefault($defaults[$idsName]);
}
$namesDefs = AttributeDefs::create($namesName)
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParamsMerged([
AttributeParam::IS_LINK_MULTIPLE_NAME_MAP => true,
'attributeRole' => 'nameMap',
'fieldType' => FieldType::LINK_MULTIPLE,
]);
$orderBy = $fieldDefs->getParam('orderBy');
$orderDirection = $fieldDefs->getParam('orderDirection');
if ($orderBy) {
$idsDefs = $idsDefs->withParam('orderBy', $orderBy);
if ($orderDirection !== null) {
$idsDefs = $idsDefs->withParam('orderDirection', $orderDirection);
}
}
$columns = $fieldDefs->getParam('columns');
$columnsDefs = $columns ?
AttributeDefs::create($columnsName)
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParamsMerged([
'columns' => $columns,
'attributeRole' => 'columnsMap',
])
: null;
$entityDefs = EntityDefs::create()
->withAttribute($idsDefs)
->withAttribute($namesDefs);
if ($columnsDefs) {
$entityDefs = $entityDefs->withAttribute($columnsDefs);
}
return $entityDefs;
}
}

View File

@@ -0,0 +1,65 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Type\AttributeType;
class LinkOne implements FieldConverter
{
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Id')
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged([
'attributeRole' => 'id',
'fieldType' => FieldType::LINK_ONE,
])
)
->withAttribute(
AttributeDefs::create($name . 'Name')
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged([
'attributeRole' => 'name',
'fieldType' => FieldType::LINK_ONE,
])
);
}
}

View File

@@ -0,0 +1,101 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Type\AttributeType;
class LinkParent implements FieldConverter
{
private const TYPE_LENGTH = 100;
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$idName = $name . 'Id';
$typeName = $name . 'Type';
$nameName = $name . 'Name';
$idDefs = AttributeDefs::create($idName)
->withType(AttributeType::FOREIGN_ID)
->withParamsMerged([
'index' => $name,
'attributeRole' => 'id',
'fieldType' => FieldType::LINK_PARENT,
]);
$typeDefs = AttributeDefs::create($typeName)
->withType(AttributeType::FOREIGN_TYPE)
->withParam(AttributeParam::NOT_NULL, false)
->withParam('index', $name)
->withLength(self::TYPE_LENGTH)
->withParamsMerged([
'attributeRole' => 'type',
'fieldType' => FieldType::LINK_PARENT,
]);
$nameDefs = AttributeDefs::create($nameName)
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged([
AttributeParam::RELATION => $name,
'isParentName' => true,
'attributeRole' => 'name',
'fieldType' => FieldType::LINK_PARENT,
]);
if ($fieldDefs->isNotStorable()) {
$idDefs = $idDefs->withNotStorable();
$typeDefs = $typeDefs->withNotStorable();
}
/** @var array<string, mixed> $defaults */
$defaults = $fieldDefs->getParam('defaultAttributes') ?? [];
if (array_key_exists($idName, $defaults)) {
$idDefs = $idDefs->withDefault($defaults[$idName]);
}
if (array_key_exists($typeName, $defaults)) {
$typeDefs = $idDefs->withDefault($defaults[$typeName]);
}
return EntityDefs::create()
->withAttribute($idDefs)
->withAttribute($typeDefs)
->withAttribute($nameDefs);
}
}

View File

@@ -0,0 +1,187 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\ORM\Type\AttributeType;
/**
* @noinspection PhpUnused
*/
class PersonName implements FieldConverter
{
private const FORMAT_LAST_FIRST = 'lastFirst';
private const FORMAT_LAST_FIRST_MIDDLE = 'lastFirstMiddle';
private const FORMAT_FIRST_MIDDLE_LAST = 'firstMiddleLast';
public function __construct(private Config $config) {}
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$format = $this->config->get('personNameFormat');
$name = $fieldDefs->getName();
$firstName = 'first' . ucfirst($name);
$lastName = 'last' . ucfirst($name);
$middleName = 'middle' . ucfirst($name);
$subList = match ($format) {
self::FORMAT_LAST_FIRST => [$lastName, ' ', $firstName],
self::FORMAT_LAST_FIRST_MIDDLE => [$lastName, ' ', $firstName, ' ', $middleName],
self::FORMAT_FIRST_MIDDLE_LAST => [$firstName, ' ', $middleName, ' ', $lastName],
default => [$firstName, ' ', $lastName],
};
if (
$format === self::FORMAT_LAST_FIRST_MIDDLE ||
$format === self::FORMAT_LAST_FIRST
) {
$orderBy1Field = $lastName;
$orderBy2Field = $firstName;
} else {
$orderBy1Field = $firstName;
$orderBy2Field = $lastName;
}
$fullList = [];
$whereItems = [];
foreach ($subList as $subFieldName) {
$fieldNameTrimmed = trim($subFieldName);
if (empty($fieldNameTrimmed)) {
$fullList[] = "'" . $subFieldName . "'";
continue;
}
$fullList[] = $fieldNameTrimmed;
$whereItems[] = $fieldNameTrimmed;
}
$whereItems[] = "CONCAT:($firstName, ' ', $lastName)";
$whereItems[] = "CONCAT:($lastName, ' ', $firstName)";
if ($format === self::FORMAT_FIRST_MIDDLE_LAST) {
$whereItems[] = "CONCAT:($firstName, ' ', $middleName, ' ', $lastName)";
} else if ($format === self::FORMAT_LAST_FIRST_MIDDLE) {
$whereItems[] = "CONCAT:($lastName, ' ', $firstName, ' ', $middleName)";
}
$selectExpression = $this->getSelect($fullList);
$selectForeignExpression = $this->getSelect($fullList, '{alias}');
if (
$format === self::FORMAT_FIRST_MIDDLE_LAST ||
$format === self::FORMAT_LAST_FIRST_MIDDLE
) {
$selectExpression = "REPLACE:($selectExpression, ' ', ' ')";
$selectForeignExpression = "REPLACE:($selectForeignExpression, ' ', ' ')";
}
$attributeDefs = AttributeDefs::create($name)
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged([
'select' => [
'select' => $selectExpression,
],
'selectForeign' => [
'select' => $selectForeignExpression,
],
'where' => [
'LIKE' => [
'whereClause' => [
'OR' => array_fill_keys(
array_map(fn ($item) => $item . '*', $whereItems),
'{value}'
),
],
],
'NOT LIKE' => [
'whereClause' => [
'AND' => array_fill_keys(
array_map(fn ($item) => $item . '!*', $whereItems),
'{value}'
),
],
],
'=' => [
'whereClause' => [
'OR' => array_fill_keys($whereItems, '{value}'),
],
],
],
'order' => [
'order' => [
[$orderBy1Field, '{direction}'],
[$orderBy2Field, '{direction}'],
],
],
]);
$dependeeAttributeList = $fieldDefs->getParam(FieldParam::DEPENDEE_ATTRIBUTE_LIST);
if ($dependeeAttributeList) {
$attributeDefs = $attributeDefs->withParam(AttributeParam::DEPENDEE_ATTRIBUTE_LIST, $dependeeAttributeList);
}
return EntityDefs::create()
->withAttribute($attributeDefs);
}
/**
* @param string[] $fullList
*/
private function getSelect(array $fullList, ?string $alias = null): string
{
foreach ($fullList as &$item) {
$rowItem = trim($item, " '");
if (empty($rowItem)) {
continue;
}
if ($alias) {
$item = $alias . '.' . $item;
}
$item = "IFNULL:($item, '')";
}
return "NULLIF:(TRIM:(CONCAT:(" . implode(", ", $fullList) . ")), '')";
}
}

View File

@@ -0,0 +1,603 @@
<?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\Utils\Database\Orm\FieldConverters;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\FieldConverter;
use Espo\Entities\PhoneNumber;
use Espo\ORM\Defs\FieldDefs;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
/**
* @noinspection PhpUnused
*/
class Phone implements FieldConverter
{
private const COLUMN_ENTITY_TYPE_LENGTH = 100;
public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
{
$name = $fieldDefs->getName();
$foreignJoinAlias = "$name$entityType{alias}Foreign";
$foreignJoinMiddleAlias = "$name$entityType{alias}ForeignMiddle";
$emailAddressDefs = AttributeDefs
::create($name)
->withType(AttributeType::VARCHAR)
->withParamsMerged(
$this->getPhoneNumberParams($entityType, $foreignJoinAlias, $foreignJoinMiddleAlias)
);
$dataDefs = AttributeDefs
::create($name . 'Data')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParamsMerged([
AttributeParam::NOT_EXPORTABLE => true,
'isPhoneNumberData' => true,
'field' => $name,
]);
$isOptedOutDefs = AttributeDefs
::create($name . 'IsOptedOut')
->withType(AttributeType::BOOL)
->withNotStorable()
->withParamsMerged(
$this->getIsOptedOutParams($foreignJoinAlias, $foreignJoinMiddleAlias)
);
$isInvalidDefs = AttributeDefs
::create($name . 'IsInvalid')
->withType(AttributeType::BOOL)
->withNotStorable()
->withParamsMerged(
$this->getIsInvalidParams($foreignJoinAlias, $foreignJoinMiddleAlias)
);
$numericAttribute = AttributeDefs
::create($name . 'Numeric')
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParamsMerged(
$this->getNumericParams($entityType)
);
$relationDefs = RelationDefs
::create('phoneNumbers')
->withType(RelationType::MANY_MANY)
->withForeignEntityType(PhoneNumber::ENTITY_TYPE)
->withRelationshipName('entityPhoneNumber')
->withMidKeys('entityId', 'phoneNumberId')
->withConditions(['entityType' => $entityType])
->withAdditionalColumn(
AttributeDefs
::create('entityType')
->withType(AttributeType::VARCHAR)
->withLength(self::COLUMN_ENTITY_TYPE_LENGTH)
)
->withAdditionalColumn(
AttributeDefs
::create('primary')
->withType(AttributeType::BOOL)
->withDefault(false)
);
return EntityDefs::create()
->withAttribute($emailAddressDefs)
->withAttribute($dataDefs)
->withAttribute($isOptedOutDefs)
->withAttribute($isInvalidDefs)
->withAttribute($numericAttribute)
->withRelation($relationDefs);
}
/**
* @return array<string, mixed>
*/
private function getPhoneNumberParams(
string $entityType,
string $foreignJoinAlias,
string $foreignJoinMiddleAlias,
): array {
return [
'select' => [
"select" => "phoneNumbers.name",
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
],
'selectForeign' => [
"select" => "$foreignJoinAlias.name",
'leftJoins' => [
[
'EntityPhoneNumber',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
PhoneNumber::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.phoneNumberId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'fieldType' => FieldType::PHONE,
'where' => [
'LIKE' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.name*' => '{value}',
],
],
],
],
'NOT LIKE' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.name*' => '{value}',
],
],
],
],
'=' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersMultiple']],
'whereClause' => [
'phoneNumbersMultiple.name=' => '{value}',
]
],
'<>' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.name' => '{value}',
],
],
],
],
'IN' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.name' => '{value}',
],
],
],
],
'NOT IN' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.name!=' => '{value}',
],
],
],
],
'IS NULL' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersMultiple']],
'whereClause' => [
'phoneNumbersMultiple.name=' => null,
]
],
'IS NOT NULL' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
],
],
],
],
],
'order' => [
'order' => [
['phoneNumbers.name', '{direction}'],
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
'additionalSelect' => ['phoneNumbers.name'],
],
];
}
/**
* @return array<string, mixed>
*/
private function getIsOptedOutParams(string $foreignJoinAlias, string $foreignJoinMiddleAlias): array
{
return [
'select' => [
'select' => 'phoneNumbers.optOut',
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
],
'selectForeign' => [
'select' => "$foreignJoinAlias.optOut",
'leftJoins' => [
[
'EntityPhoneNumber',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
PhoneNumber::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.phoneNumberId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'where' => [
'= TRUE' => [
'whereClause' => [
['phoneNumbers.optOut=' => true],
['phoneNumbers.optOut!=' => null],
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
],
'= FALSE' => [
'whereClause' => [
'OR' => [
['phoneNumbers.optOut=' => false],
['phoneNumbers.optOut=' => null],
]
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
]
],
'order' => [
'order' => [
['phoneNumbers.optOut', '{direction}'],
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
'additionalSelect' => ['phoneNumbers.optOut'],
],
];
}
/**
* @return array<string, mixed>
*/
private function getIsInvalidParams(string $foreignJoinAlias, string $foreignJoinMiddleAlias): array
{
return [
'select' => [
'select' => 'phoneNumbers.invalid',
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
],
'selectForeign' => [
'select' => "$foreignJoinAlias.invalid",
'leftJoins' => [
[
'EntityPhoneNumber',
$foreignJoinMiddleAlias,
[
"$foreignJoinMiddleAlias.entityId:" => "{alias}.id",
"$foreignJoinMiddleAlias.primary" => true,
"$foreignJoinMiddleAlias.deleted" => false,
]
],
[
PhoneNumber::ENTITY_TYPE,
$foreignJoinAlias,
[
"$foreignJoinAlias.id:" => "$foreignJoinMiddleAlias.phoneNumberId",
"$foreignJoinAlias.deleted" => false,
]
]
],
],
'where' => [
'= TRUE' => [
'whereClause' => [
['phoneNumbers.invalid=' => true],
['phoneNumbers.invalid!=' => null],
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
],
'= FALSE' => [
'whereClause' => [
'OR' => [
['phoneNumbers.invalid=' => false],
['phoneNumbers.invalid=' => null],
]
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
]
],
'order' => [
'order' => [
['phoneNumbers.invalid', '{direction}'],
],
'leftJoins' => [['phoneNumbers', 'phoneNumbers', ['primary' => true]]],
'additionalSelect' => ['phoneNumbers.invalid'],
],
];
}
/**
* @return array<string, mixed>
*/
private function getNumericParams(string $entityType): array
{
return [
AttributeParam::NOT_EXPORTABLE => true,
'where' => [
'LIKE' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric*' => '{value}',
],
],
],
],
'NOT LIKE' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
]
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric*' => '{value}',
],
],
],
],
'=' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric' => '{value}',
],
],
],
],
'<>' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric' => '{value}',
],
],
],
],
'IN' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric' => '{value}',
],
],
],
],
'NOT IN' => [
'whereClause' => [
'id!=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'joins' => [
[
'phoneNumber',
'phoneNumber',
[
'phoneNumber.id:' => 'phoneNumberId',
'phoneNumber.deleted' => false,
],
],
],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
'phoneNumber.numeric' => '{value}',
],
],
],
],
'IS NULL' => [
'leftJoins' => [['phoneNumbers', 'phoneNumbersMultiple']],
'whereClause' => [
'phoneNumbersMultiple.numeric=' => null,
]
],
'IS NOT NULL' => [
'whereClause' => [
'id=s' => [
'from' => 'EntityPhoneNumber',
'select' => ['entityId'],
'whereClause' => [
Attribute::DELETED => false,
'entityType' => $entityType,
],
],
],
],
],
];
}
}

View File

@@ -0,0 +1,40 @@
<?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\Utils\Database\Orm;
use Espo\ORM\Defs\IndexDefs;
interface IndexHelper
{
/**
* Compose an index DB name. Depending on database, the name can be unique, limited by a max length.
*/
public function composeKey(IndexDefs $defs, string $entityType): string;
}

View File

@@ -0,0 +1,55 @@
<?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\Utils\Database\Orm;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use RuntimeException;
class IndexHelperFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
public function create(string $platform): IndexHelper
{
/** @var ?class-string<IndexHelper> $className */
$className = $this->metadata
->get(['app', 'databasePlatforms', $platform, 'indexHelperClassName']);
if (!$className) {
throw new RuntimeException("No Index Helper for {$platform}");
}
return $this->injectableFactory->create($className);
}
}

View File

@@ -0,0 +1,51 @@
<?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\Utils\Database\Orm\IndexHelpers;
use Espo\Core\Utils\Database\Orm\IndexHelper;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\IndexDefs;
class MysqlIndexHelper implements IndexHelper
{
private const MAX_LENGTH = 60;
public function composeKey(IndexDefs $defs, string $entityType): string
{
$name = $defs->getName();
$prefix = $defs->isUnique() ? 'UNIQ' : 'IDX';
$parts = [$prefix, strtoupper(Util::toUnderScore($name))];
$key = implode('_', $parts);
return substr($key, 0, self::MAX_LENGTH);
}
}

View File

@@ -0,0 +1,81 @@
<?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\Utils\Database\Orm\IndexHelpers;
use Espo\Core\Utils\Database\Orm\IndexHelper;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\IndexDefs;
class PostgresqlIndexHelper implements IndexHelper
{
private const MAX_LENGTH = 59;
public function composeKey(IndexDefs $defs, string $entityType): string
{
$name = $defs->getName();
$prefix = $defs->isUnique() ? 'UNIQ' : 'IDX';
$parts = [
$prefix,
strtoupper(Util::toUnderScore($entityType)),
strtoupper(Util::toUnderScore($name)),
];
$key = implode('_', $parts);
return self::decreaseLength($key);
}
private static function decreaseLength(string $key): string
{
if (strlen($key) <= self::MAX_LENGTH) {
return $key;
}
$list = explode('_', $key);
$maxItemLength = 0;
foreach ($list as $item) {
if (strlen($item) > $maxItemLength) {
$maxItemLength = strlen($item);
}
}
$maxItemLength--;
$list = array_map(
fn ($item) => substr($item, 0, min($maxItemLength, strlen($item))),
$list
);
$key = implode('_', $list);
return self::decreaseLength($key);
}
}

View File

@@ -0,0 +1,41 @@
<?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\Utils\Database\Orm;
use Espo\ORM\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
/**
* Converts link definitions to ORM definitions.
*/
interface LinkConverter
{
public function convert(RelationDefs $linkDefs, string $entityType): EntityDefs;
}

View File

@@ -0,0 +1,70 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use LogicException;
class Attachments implements LinkConverter
{
public function __construct(private HasChildren $hasChildren) {}
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$entityDefs = $this->hasChildren->convert($linkDefs, $entityType);
$entityDefs = $entityDefs->withAttribute(
AttributeDefs::create($name . 'Types')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
);
$relationDefs = $entityDefs->getRelation($name);
if (!$relationDefs) {
throw new LogicException();
}
$relationDefs = $relationDefs->withConditions([
'OR' => [
['field' => null],
['field' => $name],
]
]);
return $entityDefs->withRelation($relationDefs);
}
}

View File

@@ -0,0 +1,97 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class BelongsTo implements LinkConverter
{
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = $linkDefs->getForeignEntityType();
$foreignRelationName = $linkDefs->hasForeignRelationName() ? $linkDefs->getForeignRelationName() : null;
$noIndex = $linkDefs->getParam('noIndex');
$noForeignName = $linkDefs->getParam('noForeignName');
$foreignName = $linkDefs->getParam('foreignName') ?? 'name';
$noJoin = $linkDefs->getParam(RelationParam::NO_JOIN);
$idName = $name . 'Id';
$nameName = $name . 'Name';
$idAttributeDefs = AttributeDefs::create($idName)
->withType(AttributeType::FOREIGN_ID)
->withParam('index', !$noIndex);
$relationDefs = RelationDefs::create($name)
->withType(RelationType::BELONGS_TO)
->withForeignEntityType($foreignEntityType)
->withKey($idName)
->withForeignKey('id')
->withForeignRelationName($foreignRelationName);
if ($linkDefs->getParam(RelationParam::DEFERRED_LOAD)) {
$relationDefs = $relationDefs->withParam(RelationParam::DEFERRED_LOAD, true);
}
$nameAttributeDefs = !$noForeignName ?
(
$noJoin ?
AttributeDefs::create($nameName)
->withType(AttributeType::VARCHAR)
->withNotStorable()
->withParam(AttributeParam::RELATION, $name)
->withParam(AttributeParam::FOREIGN, $foreignName) :
AttributeDefs::create($nameName)
->withType(AttributeType::FOREIGN)
->withNotStorable() // Used to be false before v7.4.
->withParam(AttributeParam::RELATION, $name)
->withParam(AttributeParam::FOREIGN, $foreignName)
) : null;
$entityDefs = EntityDefs::create()
->withAttribute($idAttributeDefs)
->withRelation($relationDefs);
if ($nameAttributeDefs) {
$entityDefs = $entityDefs->withAttribute($nameAttributeDefs);
}
return $entityDefs;
}
}

View File

@@ -0,0 +1,86 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class BelongsToParent implements LinkConverter
{
private const TYPE_LENGTH = 100;
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignRelationName = $linkDefs->hasForeignRelationName() ?
$linkDefs->getForeignRelationName() : null;
$idName = $name . 'Id';
$nameName = $name . 'Name';
$typeName = $name . 'Type';
$relationDefs = RelationDefs::create($name)
->withType(RelationType::BELONGS_TO_PARENT)
->withKey($idName)
->withForeignRelationName($foreignRelationName);
if ($linkDefs->getParam(RelationParam::DEFERRED_LOAD)) {
$relationDefs = $relationDefs->withParam(RelationParam::DEFERRED_LOAD, true);
}
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($idName)
->withType(AttributeType::FOREIGN_ID)
->withParam('index', $name)
)
->withAttribute(
AttributeDefs::create($typeName)
->withType(AttributeType::FOREIGN_TYPE)
->withParam(AttributeParam::NOT_NULL, false) // Revise whether needed.
->withParam('index', $name)
->withLength(self::TYPE_LENGTH)
)
->withAttribute(
AttributeDefs::create($nameName)
->withType(AttributeType::VARCHAR)
->withNotStorable()
)
->withRelation($relationDefs);
}
}

View File

@@ -0,0 +1,78 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Entities\EmailAddress;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class EmailEmailAddress implements LinkConverter
{
public function __construct() {}
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$hasField = $linkDefs->getParam('hasField');
$foreignEntityType = EmailAddress::ENTITY_TYPE;
$key1 = lcfirst($entityType) . 'Id';
$key2 = lcfirst($foreignEntityType) . 'Id';
$relationDefs = RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType($foreignEntityType)
->withKey(Attribute::ID)
->withForeignKey(Attribute::ID)
->withMidKeys($key1, $key2);
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParam('isLinkStub', !$hasField)
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParam('isLinkStub', !$hasField)
)
->withRelation($relationDefs);
}
}

View File

@@ -0,0 +1,68 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Entities\User;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
/**
* @noinspection PhpUnused
*/
class EntityCollaborator implements LinkConverter
{
private const ENTITY_TYPE_LENGTH = 100;
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$relationshipName = $linkDefs->getRelationshipName();
return EntityDefs::create()
->withRelation(
RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType(User::ENTITY_TYPE)
->withRelationshipName($relationshipName)
->withMidKeys('entityId', 'userId')
->withConditions(['entityType' => $entityType])
->withAdditionalColumn(
AttributeDefs::create('entityType')
->withType(AttributeType::VARCHAR)
->withLength(self::ENTITY_TYPE_LENGTH)
)
);
}
}

View File

@@ -0,0 +1,65 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Entities\Team;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class EntityTeam implements LinkConverter
{
private const ENTITY_TYPE_LENGTH = 100;
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$relationshipName = $linkDefs->getRelationshipName();
return EntityDefs::create()
->withRelation(
RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType(Team::ENTITY_TYPE)
->withRelationshipName($relationshipName)
->withMidKeys('entityId', 'teamId')
->withConditions(['entityType' => $entityType])
->withAdditionalColumn(
AttributeDefs::create('entityType')
->withType(AttributeType::VARCHAR)
->withLength(self::ENTITY_TYPE_LENGTH)
)
);
}
}

View File

@@ -0,0 +1,65 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Entities\User;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class EntityUser implements LinkConverter
{
private const ENTITY_TYPE_LENGTH = 100;
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$relationshipName = $linkDefs->getRelationshipName();
return EntityDefs::create()
->withRelation(
RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType(User::ENTITY_TYPE)
->withRelationshipName($relationshipName)
->withMidKeys('entityId', 'userId')
->withConditions(['entityType' => $entityType])
->withAdditionalColumn(
AttributeDefs::create('entityType')
->withType(AttributeType::VARCHAR)
->withLength(self::ENTITY_TYPE_LENGTH)
)
);
}
}

View File

@@ -0,0 +1,71 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class HasChildren implements LinkConverter
{
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = $linkDefs->getForeignEntityType();
$foreignRelationName = $linkDefs->hasForeignRelationName() ? $linkDefs->getForeignRelationName() : null;
$hasField = $linkDefs->getParam('hasField');
$relationDefs = RelationDefs::create($name)
->withType(RelationType::HAS_CHILDREN)
->withForeignEntityType($foreignEntityType)
->withForeignKey($foreignRelationName . 'Id')
->withParam('foreignType', $foreignRelationName . 'Type')
->withForeignRelationName($foreignRelationName);
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParam('isLinkStub', !$hasField) // Revise. Change to notExportable?
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParam('isLinkStub', !$hasField)
)
->withRelation($relationDefs);
}
}

View File

@@ -0,0 +1,88 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Core\Utils\Log;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class HasMany implements LinkConverter
{
public function __construct(private Log $log) {}
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = $linkDefs->getForeignEntityType();
$foreignRelationName = $linkDefs->getForeignRelationName();
$hasField = $linkDefs->getParam('hasField');
$type = RelationType::HAS_MANY;
/*$type = $linkDefs->hasRelationshipName() ?
RelationType::MANY_MANY : // Revise.
RelationType::HAS_MANY;*/
if ($linkDefs->hasRelationshipName()) {
$this->log->warning(
"Issue with the link '{$name}' in '{$entityType}' entity type. Might be the foreign link " .
"'{$foreignRelationName}' in '{$foreignEntityType}' entity type is missing. " .
"Remove the problem link manually.");
return EntityDefs::create();
}
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParam('isLinkStub', !$hasField) // Revise. Change to notExportable?
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParam('isLinkStub', !$hasField)
)
->withRelation(
RelationDefs::create($name)
->withType($type)
->withForeignEntityType($foreignEntityType)
->withForeignKey($foreignRelationName . 'Id')
->withForeignRelationName($foreignRelationName)
);
}
}

View File

@@ -0,0 +1,91 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class HasOne implements LinkConverter
{
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = $linkDefs->getForeignEntityType();
$foreignRelationName = $linkDefs->hasForeignRelationName() ? $linkDefs->getForeignRelationName() : null;
$noForeignName = $linkDefs->getParam('noForeignName');
$foreignName = $linkDefs->getParam('foreignName') ?? 'name';
$noJoin = $linkDefs->getParam('noJoin');
$idName = $name . 'Id';
$nameName = $name . 'Name';
$idAttributeDefs = AttributeDefs::create($idName)
->withType($noJoin ? AttributeType::VARCHAR : AttributeType::FOREIGN)
->withNotStorable()
->withParam(AttributeParam::RELATION, $name)
->withParam(AttributeParam::FOREIGN, Attribute::ID);
$nameAttributeDefs = !$noForeignName ?
(
AttributeDefs::create($nameName)
->withType($noJoin ? AttributeType::VARCHAR : AttributeType::FOREIGN)
->withNotStorable()
->withParam(AttributeParam::RELATION, $name)
->withParam(AttributeParam::FOREIGN, $foreignName)
) : null;
$relationDefs = RelationDefs::create($name)
->withType(RelationType::HAS_ONE)
->withForeignEntityType($foreignEntityType);
if ($foreignRelationName) {
$relationDefs = $relationDefs
->withForeignKey($foreignRelationName . 'Id')
->withForeignRelationName($foreignRelationName);
}
$entityDefs = EntityDefs::create()
->withAttribute($idAttributeDefs)
->withRelation($relationDefs);
if ($nameAttributeDefs) {
$entityDefs = $entityDefs->withAttribute($nameAttributeDefs);
}
return $entityDefs;
}
}

View File

@@ -0,0 +1,110 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class ManyMany implements LinkConverter
{
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = $linkDefs->getForeignEntityType();
$foreignRelationName = $linkDefs->getForeignRelationName();
$hasField = $linkDefs->getParam('hasField');
$columnAttributeMap = $linkDefs->getParam('columnAttributeMap');
$relationshipName = $linkDefs->hasRelationshipName() ?
$linkDefs->getRelationshipName() :
self::composeRelationshipName($entityType, $foreignEntityType);
if ($linkDefs->hasMidKey() && $linkDefs->hasForeignMidKey()) {
$key1 = $linkDefs->getMidKey();
$key2 = $linkDefs->getForeignMidKey();
} else {
$key1 = lcfirst($entityType) . 'Id';
$key2 = lcfirst($foreignEntityType) . 'Id';
if ($key1 === $key2) {
[$key1, $key2] = strcmp($name, $foreignRelationName) > 0 ?
['leftId', 'rightId'] :
['rightId', 'leftId'];
}
}
$relationDefs = RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType($foreignEntityType)
->withRelationshipName($relationshipName)
->withKey(Attribute::ID)
->withForeignKey(Attribute::ID)
->withMidKeys($key1, $key2)
->withForeignRelationName($foreignRelationName);
if ($columnAttributeMap) {
$relationDefs = $relationDefs->withParam('columnAttributeMap', $columnAttributeMap);
}
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
->withParam('isLinkStub', !$hasField) // Revise.
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
->withParam('isLinkStub', !$hasField) // Revise.
)
->withRelation($relationDefs);
}
private static function composeRelationshipName(string $left, string $right): string
{
$parts = [
Util::toCamelCase(lcfirst($left)),
Util::toCamelCase(lcfirst($right)),
];
sort($parts);
return Util::toCamelCase(implode('_', $parts));
}
}

View File

@@ -0,0 +1,73 @@
<?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\Utils\Database\Orm\LinkConverters;
use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Database\Orm\Defs\RelationDefs;
use Espo\Core\Utils\Database\Orm\LinkConverter;
use Espo\Entities\PhoneNumber;
use Espo\ORM\Defs\RelationDefs as LinkDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
class SmsPhoneNumber implements LinkConverter
{
public function __construct() {}
public function convert(LinkDefs $linkDefs, string $entityType): EntityDefs
{
$name = $linkDefs->getName();
$foreignEntityType = PhoneNumber::ENTITY_TYPE;
$key1 = lcfirst($entityType) . 'Id';
$key2 = lcfirst($foreignEntityType) . 'Id';
$relationDefs = RelationDefs::create($name)
->withType(RelationType::MANY_MANY)
->withForeignEntityType($foreignEntityType)
->withKey('id')
->withForeignKey('id')
->withMidKeys($key1, $key2);
return EntityDefs::create()
->withAttribute(
AttributeDefs::create($name . 'Ids')
->withType(AttributeType::JSON_ARRAY)
->withNotStorable()
)
->withAttribute(
AttributeDefs::create($name . 'Names')
->withType(AttributeType::JSON_OBJECT)
->withNotStorable()
)
->withRelation($relationDefs);
}
}

View File

@@ -0,0 +1,272 @@
<?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\Utils\Database\Orm;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Database\Orm\LinkConverters\BelongsTo;
use Espo\Core\Utils\Database\Orm\LinkConverters\BelongsToParent;
use Espo\Core\Utils\Database\Orm\LinkConverters\HasChildren;
use Espo\Core\Utils\Database\Orm\LinkConverters\HasMany;
use Espo\Core\Utils\Database\Orm\LinkConverters\HasOne;
use Espo\Core\Utils\Database\Orm\LinkConverters\ManyMany;
use Espo\Core\Utils\Log;
use Espo\Core\Utils\Util;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\Params\AttributeParam;
use Espo\ORM\Defs\Params\EntityParam;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Defs\RelationDefs;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
use RuntimeException;
class RelationConverter
{
private const DEFAULT_VARCHAR_LENGTH = 255;
/** @var string[] */
private $mergeParams = [
RelationParam::RELATION_NAME,
RelationParam::CONDITIONS,
RelationParam::ADDITIONAL_COLUMNS,
'noJoin',
RelationParam::INDEXES,
];
/** @var string[] */
private $manyMergeParams = [
RelationParam::ORDER_BY,
RelationParam::ORDER,
];
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private Log $log
) {}
/**
* @param string $name
* @param array<string, mixed> $params
* @param string $entityType
* @return ?array<string, mixed>
*/
public function process(string $name, array $params, string $entityType): ?array
{
$foreignEntityType = $params[RelationParam::ENTITY] ?? null;
$foreignLinkName = $params[RelationParam::FOREIGN] ?? null;
/** @var ?array<string, mixed> $foreignParams */
$foreignParams = $foreignEntityType && $foreignLinkName ?
$this->metadata->get(['entityDefs', $foreignEntityType, 'links', $foreignLinkName]) :
null;
/** @var ?string $relationshipName */
$relationshipName = $params[RelationParam::RELATION_NAME] ?? null;
if ($relationshipName) {
$relationshipName = lcfirst($relationshipName);
$params[RelationParam::RELATION_NAME] = $relationshipName;
}
$linkType = $params[RelationParam::TYPE] ?? null;
$foreignLinkType = $foreignParams ? $foreignParams[RelationParam::TYPE] : null;
if (!$linkType) {
$this->log->warning("Link $entityType.$name has no type.");
return null;
}
$params['hasField'] = (bool) $this->metadata
->get(['entityDefs', $entityType, 'fields', $name]);
$relationDefs = RelationDefs::fromRaw($params, $name);
$converter = $this->createLinkConverter($relationshipName, $linkType, $foreignLinkType);
$convertedEntityDefs = $converter->convert($relationDefs, $entityType);
$raw = $convertedEntityDefs->toAssoc();
if (isset($raw[EntityParam::RELATIONS][$name])) {
$this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? [], $linkType);
$this->correct($raw[EntityParam::RELATIONS][$name]);
}
return [$entityType => $raw];
}
private function createLinkConverter(?string $relationship, string $type, ?string $foreignType): LinkConverter
{
$className = $this->getLinkConverterClassName($relationship, $type, $foreignType);
return $this->injectableFactory->create($className);
}
/**
* @return class-string<LinkConverter>
*/
private function getLinkConverterClassName(?string $relationship, string $type, ?string $foreignType): string
{
if ($relationship) {
/** @var class-string<LinkConverter> $className */
$className = $this->metadata->get(['app', 'relationships', $relationship, 'converterClassName']);
if ($className) {
return $className;
}
}
if ($type === RelationType::HAS_MANY && $foreignType === RelationType::HAS_MANY) {
return ManyMany::class;
}
if ($type === RelationType::HAS_MANY) {
return HasMany::class;
}
if ($type === RelationType::HAS_CHILDREN) {
return HasChildren::class;
}
if ($type === RelationType::HAS_ONE) {
return HasOne::class;
}
if ($type === RelationType::BELONGS_TO) {
return BelongsTo::class;
}
if ($type === RelationType::BELONGS_TO_PARENT) {
return BelongsToParent::class;
}
throw new RuntimeException("Unsupported link type '$type'.");
}
/**
* @param array<string, mixed> $relationDefs
* @param array<string, mixed> $params
* @param array<string, mixed> $foreignParams
*/
private function mergeParams(array &$relationDefs, array $params, array $foreignParams, string $linkType): void
{
$mergeParams = $this->mergeParams;
if (
$linkType === RelationType::HAS_MANY ||
$linkType === RelationType::HAS_CHILDREN
) {
$mergeParams = array_merge($mergeParams, $this->manyMergeParams);
}
foreach ($mergeParams as $name) {
$additionalParam = $this->getMergedParam($name, $params, $foreignParams);
if ($additionalParam === null) {
continue;
}
$relationDefs[$name] = $additionalParam;
}
}
/**
* @param array<string, mixed> $params
* @param array<string, mixed> $foreignParams
* @return array<string, mixed>|scalar|null
*/
private function getMergedParam(string $name, array $params, array $foreignParams): mixed
{
$value = $params[$name] ?? null;
$foreignValue = $foreignParams[$name] ?? null;
if ($value !== null && $foreignValue !== null) {
if (!empty($value) && !is_array($value)) {
return $value;
}
if (!empty($foreignValue) && !is_array($foreignValue)) {
return $foreignValue;
}
/** @var array<int|string, mixed> $value */
/** @var array<int|string, mixed> $foreignValue */
/** @var array<string, mixed> */
return Util::merge($value, $foreignValue);
}
if (isset($value)) {
return $value;
}
if (isset($foreignValue)) {
return $foreignValue;
}
return null;
}
/**
* @param array<string, mixed> $relationDefs
*/
private function correct(array &$relationDefs): void
{
if (
isset($relationDefs[RelationParam::ORDER]) &&
is_string($relationDefs[RelationParam::ORDER])
) {
$relationDefs[RelationParam::ORDER] = strtoupper($relationDefs[RelationParam::ORDER]);
}
if (!isset($relationDefs[RelationParam::ADDITIONAL_COLUMNS])) {
return;
}
/** @var array<string, array<string, mixed>> $additionalColumns */
$additionalColumns = &$relationDefs[RelationParam::ADDITIONAL_COLUMNS];
foreach ($additionalColumns as &$columnDefs) {
$columnDefs[AttributeParam::TYPE] ??= AttributeType::VARCHAR;
if (
$columnDefs[AttributeParam::TYPE] === AttributeType::VARCHAR &&
!isset($columnDefs[AttributeParam::LEN])
) {
$columnDefs[AttributeParam::LEN] = self::DEFAULT_VARCHAR_LENGTH;
}
}
$relationDefs[RelationParam::ADDITIONAL_COLUMNS] = $additionalColumns;
}
}