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

View File

@@ -0,0 +1,89 @@
<?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\ORM;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Metadata as OrmMetadata;
use Espo\ORM\Value\AttributeExtractor;
use Espo\ORM\Value\AttributeExtractorFactory as AttributeExtractorFactoryInterface;
use RuntimeException;
/**
* @template T of object
* @implements AttributeExtractorFactoryInterface<T>
*/
class AttributeExtractorFactory implements AttributeExtractorFactoryInterface
{
public function __construct(
private Metadata $metadata,
private OrmMetadata $ormMetadata,
private InjectableFactory $injectableFactory
) {}
/**
* @return AttributeExtractor<T>
*/
public function create(string $entityType, string $field): AttributeExtractor
{
$className = $this->getClassName($entityType, $field);
if (!$className) {
throw new RuntimeException("Could not get AttributeExtractor for '{$entityType}.{$field}'.");
}
return $this->injectableFactory->createWith($className, ['entityType' => $entityType]);
}
/**
* @return ?class-string<AttributeExtractor<T>>
*/
private function getClassName(string $entityType, string $field): ?string
{
$fieldDefs = $this->ormMetadata
->getDefs()
->getEntity($entityType)
->getField($field);
$className = $fieldDefs->getParam('attributeExtractorClassName');
if ($className) {
/** @var class-string<AttributeExtractor<T>> */
return $className;
}
$type = $fieldDefs->getType();
/** @var ?class-string<AttributeExtractor<T>> */
return $this->metadata->get(['fields', $type, 'attributeExtractorClassName']);
}
}

View File

@@ -0,0 +1,161 @@
<?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\ORM;
use Espo\Core\ORM\Entity as BaseEntity;
use Espo\Core\Repositories\Database as DatabaseRepository;
use Espo\Core\Utils\ClassFinder;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity as Entity;
use Espo\ORM\EventDispatcher;
use Espo\ORM\Repository\Repository as Repository;
class ClassNameProvider
{
/** @var class-string<Entity> */
private const DEFAULT_ENTITY_CLASS_NAME = BaseEntity::class;
/** @var class-string<Repository<Entity>> */
private const DEFAULT_REPOSITORY_CLASS_NAME = DatabaseRepository::class;
/** @var array<string, class-string<Entity>> */
private array $entityCache = [];
/** @var array<string, class-string<Repository<Entity>>> */
private array $repositoryCache = [];
public function __construct(
private Metadata $metadata,
private ClassFinder $classFinder,
EventDispatcher $eventDispatcher,
) {
$eventDispatcher->subscribeToMetadataUpdate(function () {
$this->entityCache = [];
$this->repositoryCache = [];
$this->classFinder->resetRuntimeCache();
});
}
/**
* @param string $entityType
* @return class-string<Entity>
*/
public function getEntityClassName(string $entityType): string
{
if (!array_key_exists($entityType, $this->entityCache)) {
$this->entityCache[$entityType] = $this->findEntityClassName($entityType);
}
return $this->entityCache[$entityType];
}
/**
* @param string $entityType
* @return class-string<Repository<Entity>>
*/
public function getRepositoryClassName(string $entityType): string
{
if (!array_key_exists($entityType, $this->entityCache)) {
$this->repositoryCache[$entityType] = $this->findRepositoryClassName($entityType);
}
return $this->repositoryCache[$entityType];
}
/**
* @param string $entityType
* @return class-string<Entity>
*/
private function findEntityClassName(string $entityType): string
{
/** @var ?class-string<Entity> $className */
$className = $this->metadata->get("entityDefs.$entityType.entityClassName");
if ($className) {
return $className;
}
/** @var ?class-string<Entity> $className */
$className = $this->classFinder->find('Entities', $entityType);
if ($className) {
return $className;
}
/** @var ?string $template */
$template = $this->metadata->get(['scopes', $entityType, 'type']);
if ($template) {
/** @var ?class-string<Entity> $className */
$className = $this->metadata->get(['app', 'entityTemplates', $template, 'entityClassName']);
}
if ($className) {
return $className;
}
return self::DEFAULT_ENTITY_CLASS_NAME;
}
/**
* @param string $entityType
* @return class-string<Repository<Entity>>
*/
private function findRepositoryClassName(string $entityType): string
{
/** @var ?class-string<Repository<Entity>> $className */
$className = $this->metadata->get("entityDefs.$entityType.repositoryClassName");
if ($className) {
return $className;
}
/** @var ?class-string<Repository<Entity>> $className */
$className = $this->classFinder->find('Repositories', $entityType);
if ($className) {
return $className;
}
/** @var ?string $template */
$template = $this->metadata->get(['scopes', $entityType, 'type']);
if ($template) {
/** @var ?class-string<Repository<Entity>> $className */
$className = $this->metadata->get(['app', 'entityTemplates', $template, 'repositoryClassName']);
}
if ($className) {
return $className;
}
return self::DEFAULT_REPOSITORY_CLASS_NAME;
}
}

View File

@@ -0,0 +1,48 @@
<?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\ORM;
use Espo\Core\Utils\Config;
class ConfigDataProvider
{
public function __construct(private Config $config)
{}
public function logSql(): bool
{
return (bool) $this->config->get('logger.sql');
}
public function logSqlFailed(): bool
{
return (bool) $this->config->get('logger.sqlFailed');
}
}

View File

@@ -0,0 +1,95 @@
<?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\ORM;
use Espo\Core\Utils\Config;
use Espo\ORM\DatabaseParams;
use RuntimeException;
class DatabaseParamsFactory
{
private const DEFAULT_PLATFORM = 'Mysql';
public function __construct(private Config $config) {}
public function create(): DatabaseParams
{
$config = $this->config;
if (!$config->get('database')) {
throw new RuntimeException('No database params in config.');
}
$databaseParams = DatabaseParams::create()
->withHost($config->get('database.host'))
->withPort($config->get('database.port') ? (int) $config->get('database.port') : null)
->withName($config->get('database.dbname'))
->withUsername($config->get('database.user'))
->withPassword($config->get('database.password'))
->withCharset($config->get('database.charset'))
->withPlatform($config->get('database.platform'))
->withSslCa($config->get('database.sslCA'))
->withSslCert($config->get('database.sslCert'))
->withSslKey($config->get('database.sslKey'))
->withSslCaPath($config->get('database.sslCAPath'))
->withSslCipher($config->get('database.sslCipher'))
->withSslVerifyDisabled($config->get('database.sslVerifyDisabled') ?? false);
if (!$databaseParams->getPlatform()) {
$databaseParams = $databaseParams->withPlatform(self::DEFAULT_PLATFORM);
}
return $databaseParams;
}
/**
* @param array<string, mixed> $params
*/
public function createWithMergedAssoc(array $params): DatabaseParams
{
$configParams = $this->create();
return DatabaseParams::create()
->withHost($params['host'] ?? $configParams->getHost())
->withPort(isset($params['port']) ? (int) $params['port'] : $configParams->getPort())
->withName($params['dbname'] ?? $configParams->getName())
->withUsername($params['user'] ?? $configParams->getUsername())
->withPassword($params['password'] ?? $configParams->getPassword())
->withCharset($params['charset'] ?? $configParams->getCharset())
->withPlatform($params['platform'] ?? $configParams->getPlatform())
->withSslCa($params['sslCA'] ?? $configParams->getSslCa())
->withSslCert($params['sslCert'] ?? $configParams->getSslCert())
->withSslKey($params['sslKey'] ?? $configParams->getSslKey())
->withSslCaPath($params['sslCAPath'] ?? $configParams->getSslCaPath())
->withSslCipher($params['sslCipher'] ?? $configParams->getSslCipher())
->withSslVerifyDisabled($params['sslVerifyDisabled'] ?? $configParams->isSslVerifyDisabled());
}
}

View File

@@ -0,0 +1,43 @@
<?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\ORM\Defs;
class AttributeParam
{
/** @internal */
public const IS_LINK_MULTIPLE_NAME_MAP = 'isLinkMultipleNameMap';
/** @internal */
public const IS_LINK_MULTIPLE_ID_LIST = 'isLinkMultipleIdList';
/** @internal */
public const NOT_EXPORTABLE = 'notExportable';
/** @internal */
public const IS_LINK_STUB = 'isLinkStub';
}

View File

@@ -0,0 +1,729 @@
<?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\ORM;
use Espo\Core\Field\Link;
use Espo\Core\Field\LinkParent;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\ORM\BaseEntity;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity as OrmEntity;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Type\AttributeType;
use Espo\ORM\Type\RelationType;
use LogicException;
use stdClass;
/**
* An entity.
*/
class Entity extends BaseEntity
{
/**
* Has a link-multiple field.
*/
public function hasLinkMultipleField(string $field): bool
{
return
$this->hasRelation($field) &&
$this->getAttributeParam($field . 'Ids', AttributeParam::IS_LINK_MULTIPLE_ID_LIST);
}
/**
* Has a link field.
*/
public function hasLinkField(string $field): bool
{
return $this->hasAttribute($field . 'Id') && $this->hasRelation($field);
}
/**
* Has a link-parent field.
*/
public function hasLinkParentField(string $field): bool
{
return $this->getAttributeType($field . 'Type') === AttributeType::FOREIGN_TYPE &&
$this->hasAttribute($field . 'Id');
}
/**
* Load a parent-name field.
*/
public function loadParentNameField(string $field): void
{
if (!$this->hasLinkParentField($field)) {
throw new LogicException("Called `loadParentNameField` on non-link-parent field `$field`.");
}
$idAttribute = $field . 'Id';
$nameAttribute = $field . 'Name';
$parentId = $this->get($idAttribute);
$parentType = $this->get($field . 'Type');
if (!$this->entityManager) {
throw new LogicException("No entity-manager.");
}
$toSetFetched = !$this->isNew() && !$this->isAttributeChanged($idAttribute);
if (!$parentId || !$parentType) {
/** @noinspection PhpRedundantOptionalArgumentInspection */
$this->set($nameAttribute, null);
if ($toSetFetched) {
$this->setFetched($nameAttribute, null);
}
return;
}
if (!$this->entityManager->hasRepository($parentType)) {
return;
}
$repository = $this->entityManager->getRDBRepository($parentType);
$select = [Attribute::ID, Field::NAME];
$foreignEntity = $repository
->select($select)
->where([Attribute::ID => $parentId])
->findOne();
$entityName = $foreignEntity ? $foreignEntity->get(Field::NAME) : null;
$this->set($nameAttribute, $entityName);
if ($toSetFetched) {
$this->setFetched($nameAttribute, $entityName);
}
}
/**
* @param string $link
* @return ?array{
* orderBy: string|array<int, array{string, string}>|null,
* order: ?string,
* }
*/
protected function getRelationOrderParams(string $link): ?array
{
$field = $link;
$idsAttribute = $field . 'Ids';
$foreignEntityType = $this->getRelationParam($field, RelationParam::ENTITY);
if ($this->getAttributeParam($idsAttribute, 'orderBy')) {
$defs = [
'orderBy' => $this->getAttributeParam($idsAttribute, 'orderBy'),
'order' => Order::ASC,
];
if ($this->getAttributeParam($idsAttribute, 'orderDirection')) {
$defs['order'] = $this->getAttributeParam($idsAttribute, 'orderDirection');
}
return $defs;
}
if ($this->getRelationParam($link, 'orderBy')) {
$defs = [
'orderBy' => $this->getRelationParam($link, 'orderBy'),
'order' => Order::ASC,
];
if ($this->getRelationParam($link, 'order')) {
$defs['order'] = strtoupper($this->getRelationParam($link, 'order'));
}
return $defs;
}
if (!$foreignEntityType || !$this->entityManager) {
return null;
}
$ormDefs = $this->entityManager->getMetadata()->getDefs();
if (!$ormDefs->hasEntity($foreignEntityType)) {
return null;
}
$entityDefs = $ormDefs->getEntity($foreignEntityType);
$collectionDefs = $entityDefs->getParam('collection') ?? [];
$orderBy = $collectionDefs['orderBy'] ?? null;
$order = $collectionDefs['order'] ?? 'ASC';
if (!$orderBy) {
return null;
}
if (!$entityDefs->hasAttribute($orderBy)) {
return null;
}
return [
'orderBy' => $orderBy,
'order' => $order,
];
}
/**
* Load a link-multiple field. Should be used wisely. Consider using `getLinkMultipleIdList` instead.
*
* @param ?array<string, string> $columns Deprecated as of v9.0.
* @todo Add a method to load and set only fetched values?
* @internal
*/
public function loadLinkMultipleField(string $field, ?array $columns = null): void
{
if (!$this->hasLinkMultipleField($field)) {
throw new LogicException("Called `loadLinkMultipleField` on non-link-multiple field `$field`.");
}
if (!$this->entityManager) {
throw new LogicException("No entity-manager.");
}
$select = [Attribute::ID, Field::NAME];
$hasType = $this->hasAttribute($field . 'Types');
if ($hasType) {
$select[] = 'type';
}
$columns ??= $this->getLinkMultipleColumnsFromDefs($field);
if ($columns) {
foreach ($columns as $it) {
$select[] = $it;
}
}
$selectBuilder = $this->entityManager
->getRDBRepository($this->getEntityType())
->getRelation($this, $field)
->select($select);
$orderBy = null;
$order = null;
$orderParams = $this->getRelationOrderParams($field);
if ($orderParams) {
$orderBy = $orderParams['orderBy'] ?? null;
/** @var string|bool|null $order */
$order = $orderParams['order'] ?? null;
}
if ($orderBy) {
if (is_string($orderBy) && !in_array($orderBy, $select)) {
$selectBuilder->select($orderBy);
}
if (is_string($order)) {
$order = strtoupper($order);
if ($order !== Order::ASC && $order !== Order::DESC) {
$order = Order::ASC;
}
}
$selectBuilder->order($orderBy, $order);
}
$collection = $selectBuilder->find();
$ids = [];
$names = (object) [];
$types = (object) [];
$columnsData = (object) [];
foreach ($collection as $e) {
$id = $e->getId();
$ids[] = $id;
$names->$id = $e->get(Field::NAME);
if ($hasType) {
$types->$id = $e->get('type');
}
if (!$columns) {
continue;
}
$columnsData->$id = (object) [];
foreach ($columns as $column => $foreignAttribute) {
$columnsData->$id->$column = $e->get($foreignAttribute);
}
}
$idsAttribute = $field . 'Ids';
$namesAttribute = $field . 'Names';
$typesAttribute = $field . 'Types';
$columnsAttribute = $field . 'Columns';
$toSetFetched = !$this->isNew() && !$this->hasFetched($idsAttribute);
$this->setInContainerNotWritten($idsAttribute, $ids);
$this->setInContainerNotWritten($namesAttribute, $names);
if ($toSetFetched) {
$this->setFetched($idsAttribute, $ids);
$this->setFetched($namesAttribute, $names);
}
if ($hasType) {
$this->set($typesAttribute, $types);
if ($toSetFetched) {
$this->setFetched($typesAttribute, $types);
}
}
if ($columns) {
$this->setInContainerNotWritten($columnsAttribute, $columnsData);
if ($toSetFetched) {
$this->setFetched($columnsAttribute, $columnsData);
}
}
}
/**
* Load a link field. If a value is already set, it will set only a fetched value.
*/
public function loadLinkField(string $field): void
{
if (!$this->hasLinkField($field)) {
throw new LogicException("Called `loadLinkField` on non-link field '$field'.");
}
if (
$this->getRelationType($field) !== RelationType::HAS_ONE &&
$this->getRelationType($field) !== RelationType::BELONGS_TO
) {
throw new LogicException("Can't load link '$field'.");
}
if (!$this->entityManager) {
throw new LogicException("No entity-manager.");
}
$select = [Attribute::ID, Field::NAME];
$entity = $this->entityManager
->getRelation($this, $field)
->select($select)
->findOne();
$entityId = null;
$entityName = null;
if ($entity) {
$entityId = $entity->getId();
$entityName = $entity->get(Field::NAME);
}
$idAttribute = $field . 'Id';
$nameAttribute = $field . 'Name';
if (!$this->isNew() && !$this->hasFetched($idAttribute)) {
$this->setFetched($idAttribute, $entityId);
$this->setFetched($nameAttribute, $entityName);
}
if ($this->has($idAttribute)) {
return;
}
$this->setInContainerNotWritten($idAttribute, $entityId);
$this->setInContainerNotWritten($nameAttribute, $entityName);
}
/**
* Get a link-multiple name.
*/
public function getLinkMultipleName(string $field, string $id): ?string
{
$namesAttribute = $field . 'Names';
if (!$this->hasAttribute($namesAttribute)) {
throw new LogicException("Called `getLinkMultipleName` on non-link-multiple field `$field.");
}
if (!$this->has($namesAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
$object = $this->get($namesAttribute) ?? (object) [];
if (!$object instanceof stdClass) {
throw new LogicException("Non-object value in `$namesAttribute`.");
}
return $object->$id ?? null;
}
/**
* Set a link-multiple name.
*
* @return static As of v9.2.
*/
public function setLinkMultipleName(string $field, string $id, ?string $value): static
{
$namesAttribute = $field . 'Names';
if (!$this->hasAttribute($namesAttribute)) {
throw new LogicException("Called `setLinkMultipleName` on non-link-multiple field `$field.");
}
if (!$this->has($namesAttribute)) {
return $this;
}
$object = $this->get($namesAttribute) ?? (object) [];
if (!$object instanceof stdClass) {
throw new LogicException("Non-object value in `$namesAttribute`.");
}
$object->$id = $value;
$this->set($namesAttribute, $object);
return $this;
}
/**
* Get a link-multiple column value.
*/
public function getLinkMultipleColumn(string $field, string $column, string $id): mixed
{
$columnsAttribute = $field . 'Columns';
if (!$this->hasAttribute($columnsAttribute)) {
throw new LogicException("Called `getLinkMultipleColumn` on not supported field `$field.");
}
if (!$this->has($columnsAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
$object = $this->get($columnsAttribute) ?? (object) [];
if (!$object instanceof stdClass) {
throw new LogicException("Non-object value in `$columnsAttribute`.");
}
return $object->$id->$column ?? null;
}
/**
* Set a link-multiple column value.
*
* @return static As of v9.2.
*/
public function setLinkMultipleColumn(string $field, string $column, string $id, mixed $value): static
{
$columnsAttribute = $field . 'Columns';
if (!$this->hasAttribute($columnsAttribute)) {
throw new LogicException("Called `setLinkMultipleColumn` on non-link-multiple field `$field.");
}
if (!$this->has($columnsAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
$object = $this->get($columnsAttribute) ?? (object) [];
if (!$object instanceof stdClass) {
throw new LogicException("Non-object value in `$columnsAttribute`.");
}
$object->$id ??= (object) [];
$object->$id->$column = $value;
$this->set($columnsAttribute, $object);
return $this;
}
/**
* Set link-multiple IDs.
*
* @param string[] $idList
* @return static As of v9.2.
*/
public function setLinkMultipleIdList(string $field, array $idList): static
{
$idsAttribute = $field . 'Ids';
if (!$this->hasAttribute($idsAttribute)) {
throw new LogicException("Called `setLinkMultipleIdList` on non-link-multiple field `$field.");
}
$this->set($idsAttribute, $idList);
return $this;
}
/**
* Add an ID to a link-multiple field.
*
* @return static As of v9.2.
*/
public function addLinkMultipleId(string $field, string $id): static
{
$idsAttribute = $field . 'Ids';
if (!$this->hasAttribute($idsAttribute)) {
throw new LogicException("Called `addLinkMultipleId` on non-link-multiple field `$field.");
}
if (!$this->has($idsAttribute)) {
if (!$this->isNew()) {
$this->loadLinkMultipleField($field);
} else {
$this->set($idsAttribute, []);
}
}
if (!$this->has($idsAttribute)) {
return $this;
}
$idList = $this->get($idsAttribute);
if ($idList === null) {
throw new LogicException("Null value set in `$idsAttribute`.");
}
if (!is_array($idList)) {
throw new LogicException("Non-array value set in `$idsAttribute`.");
}
if (in_array($id, $idList)) {
return $this;
}
$idList[] = $id;
$this->set($idsAttribute, $idList);
return $this;
}
/**
* Remove an ID from link-multiple field.
*
* @return static As of v9.2.
*/
public function removeLinkMultipleId(string $field, string $id): static
{
if (!$this->hasLinkMultipleId($field, $id)) {
return $this;
}
$list = $this->getLinkMultipleIdList($field);
$index = array_search($id, $list);
if ($index !== false) {
unset($list[$index]);
$list = array_values($list);
}
$this->setLinkMultipleIdList($field, $list);
return $this;
}
/**
* Get link-multiple field IDs.
*
* @return string[]
*/
public function getLinkMultipleIdList(string $field): array
{
$idsAttribute = $field . 'Ids';
if (!$this->hasAttribute($idsAttribute)) {
throw new LogicException("Called `getLinkMultipleIdList` for non-link-multiple field `$field.");
}
if (!$this->has($idsAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
/** @var string[] */
return $this->get($idsAttribute) ?? [];
}
/**
* Get previous link-multiple field IDs.
*
* @return string[]
* @since 9.1.0
*/
public function getFetchedLinkMultipleIdList(string $field): array
{
$idsAttribute = $field . 'Ids';
if (!$this->hasAttribute($idsAttribute)) {
throw new LogicException("Called `getFetchedLinkMultipleIdList` for non-link-multiple field `$field.");
}
if (!$this->isNew()) {
if (!$this->has($idsAttribute)) {
$this->loadLinkMultipleField($field);
} else if (!$this->hasFetched($field)) {
// Set but not loaded.
$attributes = [
$field . 'Ids',
$field . 'Names',
$field . 'Types',
$field . 'Columns',
];
$map = array_reduce($attributes, function ($p, $item) {
if (!$this->has($item)) {
return $p;
}
$p[$item] = $this->get($item);
return $p;
}, []);
$this->loadLinkMultipleField($field);
// Restore set values.
$this->setMultiple($map);
}
}
if (!$this->has($idsAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
/** @var string[] */
return $this->getFetched($idsAttribute) ?? [];
}
/**
* Has an ID in a link-multiple field.
*/
public function hasLinkMultipleId(string $field, string $id): bool
{
$idsAttribute = $field . 'Ids';
if (!$this->hasAttribute($idsAttribute)) {
throw new LogicException("Called `hasLinkMultipleId` for non-link-multiple field `$field.");
}
if (!$this->has($idsAttribute) && !$this->isNew()) {
$this->loadLinkMultipleField($field);
}
if (!$this->has($idsAttribute)) {
return false;
}
/** @var string[] $idList */
$idList = $this->get($idsAttribute) ?? [];
return in_array($id, $idList);
}
/**
* @return string[]|null
*/
private function getLinkMultipleColumnsFromDefs(string $field): ?array
{
if (!$this->entityManager) {
return null;
}
$entityDefs = $this->entityManager->getDefs()->getEntity($this->entityType);
/** @var ?array<string, string> $columns */
$columns = $entityDefs->tryGetField($field)?->getParam('columns');
if (!$columns) {
return $columns;
}
$foreignEntityType = $entityDefs->tryGetRelation($field)?->tryGetForeignEntityType();
if ($foreignEntityType) {
$foreignEntityDefs = $this->entityManager->getDefs()->getEntity($foreignEntityType);
foreach ($columns as $column => $attribute) {
if (!$foreignEntityDefs->hasAttribute($attribute)) {
// For backward compatibility. If foreign attributes defined in the field do not exist.
unset($columns[$column]);
}
}
}
return $columns;
}
/**
* @since 9.0.0
*/
protected function setRelatedLinkOrEntity(string $relation, Link|LinkParent|OrmEntity|null $related): static
{
if ($related instanceof Entity || $related === null) {
$this->relations->set($relation, $related);
return $this;
}
$this->setValueObject($relation, $related);
return $this;
}
}

View File

@@ -0,0 +1,188 @@
<?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\ORM;
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Binding\BindingData;
use Espo\Core\InjectableFactory;
use Espo\ORM\DataLoader\Loader;
use Espo\ORM\DataLoader\RDBLoader;
use Espo\ORM\Entity;
use Espo\ORM\EntityFactory as EntityFactoryInterface;
use Espo\ORM\EntityManager;
use Espo\ORM\Relation\RDBRelations;
use Espo\ORM\Relation\Relations;
use Espo\ORM\Relation\RelationsMap;
use Espo\ORM\Value\ValueAccessorFactory;
use RuntimeException;
class EntityFactory implements EntityFactoryInterface
{
private ?EntityManager $entityManager = null;
private ?ValueAccessorFactory $valueAccessorFactory = null;
public function __construct(
private ClassNameProvider $classNameProvider,
private Helper $helper,
private InjectableFactory $injectableFactory,
private RelationsMap $relationsMap,
) {}
public function setEntityManager(EntityManager $entityManager): void
{
if ($this->entityManager) {
throw new RuntimeException("EntityManager can be set only once.");
}
$this->entityManager = $entityManager;
}
public function setValueAccessorFactory(ValueAccessorFactory $valueAccessorFactory): void
{
if ($this->valueAccessorFactory) {
throw new RuntimeException("ValueAccessorFactory can be set only once.");
}
$this->valueAccessorFactory = $valueAccessorFactory;
}
public function create(string $entityType): Entity
{
return $this->createInternal($entityType);
}
/**
* @param ?array<string, mixed> $attributeDefs
*/
private function createInternal(string $entityType, ?array $attributeDefs = null): Entity
{
$className = $this->getClassName($entityType);
if (!$this->entityManager) {
throw new RuntimeException("No entityManager.");
}
$defs = $this->entityManager->getMetadata()->get($entityType);
if (is_null($defs)) {
throw new RuntimeException("Entity '$entityType' is not defined in metadata.");
}
if ($attributeDefs) {
$defs['attributes'] = array_merge($defs['attributes'] ?? [], $attributeDefs);
}
$relations = $this->injectableFactory->createWithBinding(
RDBRelations::class,
BindingContainerBuilder::create()
->bindInstance(EntityManager::class, $this->entityManager)
->build()
);
$loader = $this->injectableFactory->createWithBinding(
RDBLoader::class,
BindingContainerBuilder::create()
->bindInstance(EntityManager::class, $this->entityManager)
->build()
);
$bindingContainer = $this->getBindingContainer(
$className,
$entityType,
$defs,
$relations,
$loader,
);
$entity = $this->injectableFactory->createWithBinding($className, $bindingContainer);
if ($relations instanceof RDBRelations) {
$relations->setEntity($entity);
}
$this->relationsMap->set($entity, $relations);
return $entity;
}
/**
* @param array<string, mixed> $attributeDefs
* @internal
* @noinspection PhpUnused
*/
public function createWithAdditionalAttributes(string $entityType, array $attributeDefs): Entity
{
return $this->createInternal($entityType, $attributeDefs);
}
/**
* @return class-string<Entity>
*/
private function getClassName(string $entityType): string
{
/** @var class-string<Entity> */
return $this->classNameProvider->getEntityClassName($entityType);
}
/**
* @param class-string<Entity> $className
* @param array<string, mixed> $defs
*/
private function getBindingContainer(
string $className,
string $entityType,
array $defs,
Relations $relations,
Loader $loader,
): BindingContainer {
if (!$this->entityManager || !$this->valueAccessorFactory) {
throw new RuntimeException();
}
$data = new BindingData();
$binder = new Binder($data);
$binder
->for($className)
->bindValue('$entityType', $entityType)
->bindValue('$defs', $defs)
->bindInstance(EntityManager::class, $this->entityManager)
->bindInstance(ValueAccessorFactory::class, $this->valueAccessorFactory)
->bindInstance(Helper::class, $this->helper)
->bindInstance(Relations::class, $relations)
->bindInstance(Loader::class, $loader);
return new BindingContainer($data);
}
}

View File

@@ -0,0 +1,35 @@
<?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\ORM;
use Espo\ORM\EntityManager as BaseEntityManager;
class EntityManager extends BaseEntityManager
{}

View File

@@ -0,0 +1,170 @@
<?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\ORM;
use Espo\Core\ORM\PDO\PDOFactoryFactory;
use Espo\Core\ORM\QueryComposer\QueryComposerFactory;
use Espo\Core\InjectableFactory;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\ORM\QueryComposer\Part\FunctionConverterFactory;
use Espo\Core\Utils\Log;
use Espo\ORM\Executor\DefaultSqlExecutor;
use Espo\ORM\Metadata;
use Espo\ORM\EventDispatcher;
use Espo\ORM\DatabaseParams;
use Espo\ORM\PDO\PDOFactory;
use Espo\ORM\QueryComposer\QueryComposerFactory as QueryComposerFactoryInterface;
use Espo\ORM\Relation\RelationsMap;
use Espo\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
use Espo\ORM\EntityFactory as EntityFactoryInterface;
use Espo\ORM\Executor\SqlExecutor;
use Espo\ORM\Value\ValueFactoryFactory as ValueFactoryFactoryInterface;
use Espo\ORM\Value\AttributeExtractorFactory as AttributeExtractorFactoryInterface;
use Espo\ORM\PDO\PDOProvider;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory as FunctionConverterFactoryInterface;
use RuntimeException;
class EntityManagerFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private MetadataDataProvider $metadataDataProvider,
private EventDispatcher $eventDispatcher,
private PDOFactoryFactory $pdoFactoryFactory,
private DatabaseParamsFactory $databaseParamsFactory,
private ConfigDataProvider $configDataProvider,
private Log $log,
) {}
public function create(): EntityManager
{
$relationsMap = new RelationsMap();
$entityFactory = $this->injectableFactory->createWithBinding(
EntityFactory::class,
BindingContainerBuilder::create()
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->bindInstance(RelationsMap::class, $relationsMap)
->build()
);
$repositoryFactory = $this->injectableFactory->createWithBinding(
RepositoryFactory::class,
BindingContainerBuilder::create()
->bindInstance(EntityFactoryInterface::class, $entityFactory)
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->bindInstance(RelationsMap::class, $relationsMap)
->build()
);
$databaseParams = $this->createDatabaseParams();
$metadata = new Metadata($this->metadataDataProvider, $this->eventDispatcher);
$valueFactoryFactory = $this->injectableFactory->createWithBinding(
ValueFactoryFactory::class,
BindingContainerBuilder::create()
->bindInstance(Metadata::class, $metadata)
->build()
);
$attributeExtractorFactory = $this->injectableFactory->createWithBinding(
AttributeExtractorFactory::class,
BindingContainerBuilder::create()
->bindInstance(Metadata::class, $metadata)
->build()
);
$functionConverterFactory = $this->injectableFactory->createWithBinding(
FunctionConverterFactory::class,
BindingContainerBuilder::create()
->bindInstance(DatabaseParams::class, $databaseParams)
->build()
);
$pdoFactory = $this->pdoFactoryFactory->create($databaseParams->getPlatform() ?? '');
$pdoProvider = $this->injectableFactory->createResolved(
PDOProvider::class,
BindingContainerBuilder::create()
->bindInstance(DatabaseParams::class, $databaseParams)
->bindInstance(PDOFactory::class, $pdoFactory)
->build()
);
$queryComposerFactory = $this->injectableFactory->createWithBinding(
QueryComposerFactory::class,
BindingContainerBuilder::create()
->bindInstance(PDOProvider::class, $pdoProvider)
->bindInstance(Metadata::class, $metadata)
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->bindInstance(EntityFactoryInterface::class, $entityFactory)
->bindInstance(FunctionConverterFactoryInterface::class, $functionConverterFactory)
->build()
);
$sqlExecutor = new DefaultSqlExecutor(
$pdoProvider,
$this->log,
$this->configDataProvider->logSql(),
$this->configDataProvider->logSqlFailed()
);
$binding = BindingContainerBuilder::create()
->bindInstance(DatabaseParams::class, $databaseParams)
->bindInstance(Metadata::class, $metadata)
->bindInstance(QueryComposerFactoryInterface::class, $queryComposerFactory)
->bindInstance(RepositoryFactoryInterface::class, $repositoryFactory)
->bindInstance(EntityFactoryInterface::class, $entityFactory)
->bindInstance(ValueFactoryFactoryInterface::class, $valueFactoryFactory)
->bindInstance(AttributeExtractorFactoryInterface::class, $attributeExtractorFactory)
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->bindInstance(PDOProvider::class, $pdoProvider)
->bindInstance(FunctionConverterFactoryInterface::class, $functionConverterFactory)
->bindInstance(SqlExecutor::class, $sqlExecutor)
->bindInstance(RelationsMap::class, $relationsMap)
->build();
return $this->injectableFactory->createWithBinding(EntityManager::class, $binding);
}
private function createDatabaseParams(): DatabaseParams
{
$databaseParams = $this->databaseParamsFactory->create();
if (!$databaseParams->getName()) {
throw new RuntimeException('No database name specified in config.');
}
return $databaseParams;
}
}

View File

@@ -0,0 +1,132 @@
<?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\ORM;
use Espo\ORM\Entity;
use Espo\ORM\Metadata;
use Espo\ORM\Repository\RDBRepository;
use Espo\ORM\Repository\Repository;
use Espo\ORM\Executor\SqlExecutor;
use Espo\Core\Container;
class EntityManagerProxy
{
private ?EntityManager $entityManager = null;
public function __construct(private Container $container)
{}
private function getEntityManager(): EntityManager
{
if (!$this->entityManager) {
$this->entityManager = $this->container->getByClass(EntityManager::class);
}
return $this->entityManager;
}
public function getNewEntity(string $entityType): Entity
{
return $this->getEntityManager()->getNewEntity($entityType);
}
public function getEntityById(string $entityType, string $id): ?Entity
{
return $this->getEntityManager()->getEntityById($entityType, $id);
}
/**
* @deprecated As of v9.0.
* @todo Remove in v11.0.
*/
public function getEntity(string $entityType, ?string $id = null): ?Entity
{
/** @noinspection PhpDeprecationInspection */
return $this->getEntityManager()->getEntity($entityType, $id);
}
/**
* @param array<string, mixed> $options
*/
public function saveEntity(Entity $entity, array $options = []): void
{
$this->getEntityManager()->saveEntity($entity, $options);
}
/**
* @return Repository<Entity>
*/
public function getRepository(string $entityType): Repository
{
return $this->getEntityManager()->getRepository($entityType);
}
/**
* @return RDBRepository<Entity>
*/
public function getRDBRepository(string $entityType): RDBRepository
{
return $this->getEntityManager()->getRDBRepository($entityType);
}
public function getMetadata(): Metadata
{
return $this->getEntityManager()->getMetadata();
}
public function getSqlExecutor(): SqlExecutor
{
return $this->getEntityManager()->getSqlExecutor();
}
/**
* Get an RDB repository by an entity class name.
*
* @template T of Entity
* @param class-string<T> $className An entity class name.
* @return RDBRepository<T>
*/
public function getRDBRepositoryByClass(string $className): RDBRepository
{
return $this->getEntityManager()->getRDBRepositoryByClass($className);
}
/**
* Get a repository by an entity class name.
*
* @template T of Entity
* @param class-string<T> $className An entity class name.
* @return Repository<T>
*/
public function getRepositoryByClass(string $className): Repository
{
return $this->getEntityManager()->getRepositoryByClass($className);
}
}

View File

@@ -0,0 +1,160 @@
<?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\ORM;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
class Helper
{
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)
{}
/**
* @internal
*/
public function hasAllPersonNameAttributes(Entity $entity, string $field): bool
{
$format = $this->config->get('personNameFormat');
$firstName = 'first' . ucfirst($field);
$lastName = 'last' . ucfirst($field);
$middleName = 'middle' . ucfirst($field);
$attributes = [
$firstName,
$lastName,
];
if (
$format === self::FORMAT_LAST_FIRST_MIDDLE ||
$format === self::FORMAT_FIRST_MIDDLE_LAST
) {
$attributes[] = $middleName;
}
foreach ($attributes as $attribute) {
if (!$entity->has($attribute)) {
return false;
}
}
return true;
}
/**
* @internal
*/
public function formatPersonName(Entity $entity, string $field): ?string
{
$format = $this->config->get('personNameFormat');
$first = $entity->get('first' . ucfirst($field));
$last = $entity->get('last' . ucfirst($field));
$middle = $entity->get('middle' . ucfirst($field));
switch ($format) {
case self::FORMAT_LAST_FIRST:
if ($first === null && $last === null) {
return null;
}
if ($first === null) {
return $last;
}
if ($last === null) {
return $first;
}
return $last . ' ' . $first;
case self::FORMAT_LAST_FIRST_MIDDLE:
if ($first === null && $last === null && $middle === null) {
return null;
}
$arr = [];
if ($last !== null) {
$arr[] = $last;
}
if ($first !== null) {
$arr[] = $first;
}
if ($middle !== null) {
$arr[] = $middle;
}
return implode(' ', $arr);
case self::FORMAT_FIRST_MIDDLE_LAST:
if (!$first && !$last && !$middle) {
return null;
}
$arr = [];
if ($first !== null) {
$arr[] = $first;
}
if ($middle !== null) {
$arr[] = $middle;
}
if ($last !== null) {
$arr[] = $last;
}
return implode(' ', $arr);
}
if ($first === null && $last === null) {
return null;
}
if ($first === null) {
return $last;
}
if ($last === null) {
return $first;
}
return $first . ' ' . $last;
}
}

View File

@@ -0,0 +1,53 @@
<?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\ORM;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Metadata\OrmMetadataData;
use Espo\ORM\MetadataDataProvider as MetadataDataProviderInterface;
class MetadataDataProvider implements MetadataDataProviderInterface
{
public function __construct(
private OrmMetadataData $ormMetadataData,
private Metadata $metadata
) {}
public function get(): array
{
$data = $this->ormMetadataData->getData();
foreach (array_keys($data) as $entityType) {
$data[$entityType]['fields'] = $this->metadata->get(['entityDefs', $entityType, 'fields']) ?? [];
}
return $data;
}
}

View File

@@ -0,0 +1,57 @@
<?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\ORM\PDO;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\PDO\PDOFactory;
use RuntimeException;
class PDOFactoryFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
public function create(string $platform): PDOFactory
{
/** @var ?class-string<PDOFactory> $className */
$className =
$this->metadata->get(['app', 'orm', 'platforms', $platform, 'pdoFactoryClassName']) ??
$this->metadata->get(['app', 'orm', 'pdoFactoryClassNameMap', $platform]);
if (!$className) {
throw new RuntimeException("Could not create PDOFactory.");
}
return $this->injectableFactory->create($className);
}
}

View File

@@ -0,0 +1,89 @@
<?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\ORM\QueryComposer\Part;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory as FunctionConverterFactoryInterface;
use Espo\ORM\QueryComposer\Part\FunctionConverter;
use Espo\ORM\DatabaseParams;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use LogicException;
class FunctionConverterFactory implements FunctionConverterFactoryInterface
{
/** @var array<string, FunctionConverter> */
private $hash = [];
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private DatabaseParams $databaseParams
) {}
public function create(string $name): FunctionConverter
{
$className = $this->getClassName($name);
if ($className === null) {
throw new LogicException();
}
return $this->injectableFactory->create($className);
}
public function isCreatable(string $name): bool
{
if ($this->getClassName($name) === null) {
return false;
}
return true;
}
/**
* @return ?class-string<FunctionConverter>
*/
private function getClassName(string $name): ?string
{
if (!array_key_exists($name, $this->hash)) {
/** @var string $platform */
$platform = $this->databaseParams->getPlatform();
$this->hash[$name] =
$this->metadata->get(['app', 'orm', 'platforms', $platform, 'functionConverterClassNameMap', $name]) ??
$this->metadata->get(['app', 'orm', 'functionConverterClassNameMap_' . $platform, $name]);
}
/** @var ?class-string<FunctionConverter> */
return $this->hash[$name];
}
}

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\ORM\QueryComposer\Part\FunctionConverters;
use Espo\ORM\QueryComposer\Part\FunctionConverter;
class Abs implements FunctionConverter
{
public function convert(string ...$argumentList): string
{
return 'ABS(' . implode(', ', $argumentList) . ')';
}
}

View File

@@ -0,0 +1,83 @@
<?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\ORM\QueryComposer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\InjectableFactory;
use Espo\ORM\EventDispatcher;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\PDO\PDOProvider;
use Espo\ORM\QueryComposer\QueryComposer;
use Espo\ORM\Metadata as OrmMetadata;
use Espo\ORM\EntityFactory;
use PDO;
use RuntimeException;
class QueryComposerFactory implements \Espo\ORM\QueryComposer\QueryComposerFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private PDOProvider $pdoProvider,
private OrmMetadata $ormMetadata,
private EntityFactory $entityFactory,
private FunctionConverterFactory $functionConverterFactory,
private EventDispatcher $eventDispatcher
) {}
public function create(string $platform): QueryComposer
{
/** @var ?class-string<QueryComposer> $className */
$className =
$this->metadata->get(['app', 'orm', 'platforms', $platform, 'queryComposerClassName']) ??
$this->metadata->get(['app', 'orm', 'queryComposerClassNameMap', $platform]);
if (!$className) {
/** @var class-string<QueryComposer> $className */
$className = "Espo\\ORM\\QueryComposer\\{$platform}QueryComposer";
}
if (!class_exists($className)) {
throw new RuntimeException("Query composer for '{$platform}' platform does not exist.");
}
$bindingContainer = BindingContainerBuilder::create()
->bindInstance(PDO::class, $this->pdoProvider->get())
->bindInstance(OrmMetadata::class, $this->ormMetadata)
->bindInstance(EntityFactory::class, $this->entityFactory)
->bindInstance(FunctionConverterFactory::class, $this->functionConverterFactory)
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->build();
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
}
}

View File

@@ -0,0 +1,121 @@
<?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\ORM\Repository;
use Espo\ORM\Entity;
use Espo\ORM\Query\Select;
use Espo\ORM\Repository\EmptyHookMediator;
use Espo\Core\HookManager;
use Espo\Core\ORM\Repository\Option\SaveOption;
class HookMediator extends EmptyHookMediator
{
public function __construct(protected HookManager $hookManager)
{}
/**
* @param ?array<string, mixed> $columnData
* @param array<string, mixed> $options
*/
public function afterRelate(
Entity $entity,
string $relationName,
Entity $foreignEntity,
?array $columnData,
array $options
): void {
if (!empty($options[SaveOption::SKIP_HOOKS])) {
return;
}
$hookData = [
'relationName' => $relationName,
'relationData' => $columnData,
'foreignEntity' => $foreignEntity,
'foreignId' => $foreignEntity->getId(),
];
$this->hookManager->process(
$entity->getEntityType(),
'afterRelate',
$entity,
$options,
$hookData
);
}
/**
* @param array<string, mixed> $options
*/
public function afterUnrelate(Entity $entity, string $relationName, Entity $foreignEntity, array $options): void
{
if (!empty($options[Option\SaveOption::SKIP_HOOKS])) {
return;
}
$hookData = [
'relationName' => $relationName,
'foreignEntity' => $foreignEntity,
'foreignId' => $foreignEntity->getId(),
];
$this->hookManager->process(
$entity->getEntityType(),
'afterUnrelate',
$entity,
$options,
$hookData
);
}
/**
* @param array<string, mixed> $options
*/
public function afterMassRelate(Entity $entity, string $relationName, Select $query, array $options): void
{
if (!empty($options[SaveOption::SKIP_HOOKS])) {
return;
}
$hookData = [
'relationName' => $relationName,
'query' => $query,
];
$this->hookManager->process(
$entity->getEntityType(),
'afterMassRelate',
$entity,
$options,
$hookData
);
}
}

View File

@@ -0,0 +1,59 @@
<?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\ORM\Repository\Option;
/**
* Save options.
*
* @since 9.2.0
*/
class RemoveOption
{
/**
* Silent. Boolean.
* Skip stream notes, notifications, webhooks.
*/
public const SILENT = 'silent';
/**
* Called from a Record service.
*/
public const API = 'api';
/**
* When saved in Mass-Remove.
*/
public const MASS_REMOVE = 'massRemove';
/**
* Override modified-by. String.
*/
public const MODIFIED_BY_ID = 'modifiedById';
}

View File

@@ -0,0 +1,162 @@
<?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\ORM\Repository\Option;
use Closure;
use Espo\Core\Utils\Util;
use Espo\ORM\Repository\Option\SaveOptions;
/**
* A save context.
*
* If a save invokes another save, the context instance should not be re-used.
* If a save invokes a relate action, the context can be passed to that action.
*
* @since 9.1.0
*/
class SaveContext
{
public const NAME = 'context';
private string $actionId;
private bool $linkUpdated = false;
/** @var Closure[] */
private array $deferredActions = [];
/**
* @param ?string $actionId An action ID.
*/
public function __construct(
?string $actionId = null,
) {
$this->actionId = $actionId ?? Util::generateId();
}
/**
* An action ID. Used to group notifications. If a save invokes another save, the same ID can be re-used,
* but the context instance should not be re-used. Create a derived context for this.
*
* @since 9.2.0
*/
public function getActionId(): string
{
return $this->actionId;
}
/**
* @deprecated Since v9.2.0. Use `getActionId`.
*/
public function getId(): string
{
return $this->getActionId();
}
public function setLinkUpdated(): self
{
$this->linkUpdated = true;
return $this;
}
public function isLinkUpdated(): bool
{
return $this->linkUpdated;
}
/**
* Obtain from save options.
*
* @return ?self
* @since 9.2.0.
*/
public static function obtainFromOptions(SaveOptions $options): ?self
{
$saveContext = $options->get(self::NAME);
if (!$saveContext instanceof self) {
return null;
}
return $saveContext;
}
/**
* Obtain from raw save options.
*
* @param array<string, mixed> $options
* @return ?self
* @since 9.2.0.
*/
public static function obtainFromRawOptions(array $options): ?self
{
$saveContext = $options[self::NAME] ?? null;
if (!$saveContext instanceof self) {
return null;
}
return $saveContext;
}
/**
* Add a deferred action.
*
* @param Closure $callback A callback.
* @since 9.2.0.
*/
public function addDeferredAction(Closure $callback): void
{
$this->deferredActions[] = $callback;
}
/**
* @internal
* @since 9.2.0.
*/
public function callDeferredActions(): void
{
foreach ($this->deferredActions as $callback) {
$callback();
}
$this->deferredActions = [];
}
/**
* Create a derived context. To be used for nested saves.
*
* @since 9.2.0
*/
public function createDerived(): self
{
return new self($this->actionId);
}
}

View File

@@ -0,0 +1,117 @@
<?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\ORM\Repository\Option;
use Espo\ORM\Repository\Option\SaveOption as BaseSaveOption;
/**
* Save options.
*/
class SaveOption
{
/**
* Silent. Boolean.
* Skip stream notes, notifications, webhooks.
*/
public const SILENT = 'silent';
/**
* Import. Boolean.
*/
public const IMPORT = 'import';
/**
* Called from a Record service.
* @since 8.0.1
*/
public const API = 'api';
/**
* Skip all additional processing. Boolean.
*/
public const SKIP_ALL = BaseSaveOption::SKIP_ALL;
/**
* Keep new. Boolean.
*/
public const KEEP_NEW = BaseSaveOption::KEEP_NEW;
/**
* Keep dirty. Boolean.
*/
public const KEEP_DIRTY = BaseSaveOption::KEEP_DIRTY;
/**
* Keep an entity relations map. Boolean.
* @since 9.0.0
*/
public const KEEP_RELATIONS = BaseSaveOption::KEEP_RELATIONS;
/**
* Skip hooks. Boolean.
*/
public const SKIP_HOOKS = 'skipHooks';
/**
* Skip setting created-by. Boolean.
*/
public const SKIP_CREATED_BY = 'skipCreatedBy';
/**
* Skip setting modified-by. Boolean.
*/
public const SKIP_MODIFIED_BY = 'skipModifiedBy';
/**
* Override created-by. String.
*/
public const CREATED_BY_ID = 'createdById';
/**
* Override modified-by. String.
*/
public const MODIFIED_BY_ID = 'modifiedById';
/**
* A duplicate source ID. A record that is being duplicated.
* @since 8.4.0
*/
public const DUPLICATE_SOURCE_ID = 'duplicateSourceId';
/**
* When saved in Mass-Update.
* @since 8.4.0
*/
public const MASS_UPDATE = 'massUpdate';
/**
* Skip stream notes. Boolean.
* @since 9.0.0
*/
public const NO_STREAM = 'noStream';
/**
* Skip notification. Boolean.
* @since 9.0.0
*/
public const NO_NOTIFICATIONS = 'noNotifications';
/**
* Skip audit log records.
* @since 9.1.0
*/
public const SKIP_AUDITED = 'skipAudited';
}

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\ORM;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Binding\ContextualBinder;
use Espo\Core\InjectableFactory;
use Espo\ORM\Entity as Entity;
use Espo\ORM\EntityFactory as EntityFactoryInterface;
use Espo\ORM\Relation\RelationsMap;
use Espo\ORM\Repository\Repository;
use Espo\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
class RepositoryFactory implements RepositoryFactoryInterface
{
public function __construct(
private EntityFactoryInterface $entityFactory,
private InjectableFactory $injectableFactory,
private ClassNameProvider $classNameProvider,
private RelationsMap $relationsMap,
) {}
public function create(string $entityType): Repository
{
$className = $this->getClassName($entityType);
return $this->injectableFactory->createWithBinding(
$className,
BindingContainerBuilder::create()
->bindInstance(EntityFactoryInterface::class, $this->entityFactory)
->bindInstance(EntityFactory::class, $this->entityFactory)
->bindInstance(RelationsMap::class, $this->relationsMap)
->inContext(
$className,
function (ContextualBinder $binder) use ($entityType) {
$binder->bindValue('$entityType', $entityType);
}
)
->build()
);
}
/**
* @return class-string<Repository<Entity>>
*/
private function getClassName(string $entityType): string
{
/** @var class-string<Repository<Entity>> */
return $this->classNameProvider->getRepositoryClassName($entityType);
}
}

View File

@@ -0,0 +1,67 @@
<?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\ORM\Type;
class FieldType
{
public const VARCHAR = 'varchar';
public const BOOL = 'bool';
public const TEXT = 'text';
public const INT = 'int';
public const FLOAT = 'float';
public const DATE = 'date';
public const DATETIME = 'datetime';
public const DATETIME_OPTIONAL = 'datetimeOptional';
public const ENUM = 'enum';
public const MULTI_ENUM = 'multiEnum';
public const ARRAY = 'array';
public const CHECKLIST = 'checklist';
public const CURRENCY = 'currency';
public const CURRENCY_CONVERTED = 'currencyConverted';
public const PERSON_NAME = 'personName';
public const ADDRESS = 'address';
public const EMAIL = 'email';
public const PHONE = 'phone';
public const AUTOINCREMENT = 'autoincrement';
public const URL = 'url';
public const NUMBER = 'number';
public const LINK = 'link';
public const LINK_ONE = 'linkOne';
public const LINK_PARENT = 'linkParent';
public const FILE = 'file';
public const IMAGE = 'image';
public const LINK_MULTIPLE = 'linkMultiple';
public const ATTACHMENT_MULTIPLE = 'attachmentMultiple';
public const FOREIGN = 'foreign';
public const WYSIWYG = 'wysiwyg';
public const JSON_ARRAY = 'jsonArray';
public const JSON_OBJECT = 'jsonObject';
public const PASSWORD = 'password';
}

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\ORM;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Metadata as OrmMetadata;
use Espo\ORM\Value\ValueFactory;
use Espo\ORM\Value\ValueFactoryFactory as ValueFactoryFactoryInteface;
use RuntimeException;
class ValueFactoryFactory implements ValueFactoryFactoryInteface
{
public function __construct(
private Metadata $metadata,
private OrmMetadata $ormMetadata,
private InjectableFactory $injectableFactory
) {}
public function isCreatable(string $entityType, string $field): bool
{
return $this->getClassName($entityType, $field) !== null;
}
public function create(string $entityType, string $field): ValueFactory
{
$className = $this->getClassName($entityType, $field);
if (!$className) {
throw new RuntimeException("Could not get ValueFactory for '{$entityType}.{$field}'.");
}
return $this->injectableFactory->create($className);
}
/**
* @return ?class-string<ValueFactory>
*/
private function getClassName(string $entityType, string $field): ?string
{
$fieldDefs = $this->ormMetadata
->getDefs()
->getEntity($entityType)
->getField($field);
/** @var ?class-string<ValueFactory> $className */
$className = $fieldDefs->getParam('valueFactoryClassName');
if ($className) {
return $className;
}
$type = $fieldDefs->getType();
/** @var ?class-string<ValueFactory> */
return $this->metadata->get(['fields', $type, 'valueFactoryClassName']);
}
}