entity defs field loaders

This commit is contained in:
Yuri Kuznetsov
2025-05-01 17:22:13 +03:00
parent 5b9ac837dd
commit 4418133ec0
11 changed files with 213 additions and 24 deletions
@@ -32,7 +32,9 @@ namespace Espo\Core\FieldProcessing;
use Espo\Core\Acl;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Utils\FieldUtil;
use Espo\Entities\User;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\Core\FieldProcessing\Loader\Params;
@@ -53,7 +55,9 @@ class ListLoadProcessor
private InjectableFactory $injectableFactory,
private Metadata $metadata,
private Acl $acl,
private User $user
private User $user,
private Defs $defs,
private FieldUtil $fieldUtil,
) {
$this->bindingContainer = BindingContainerBuilder::create()
->bindInstance(User::class, $this->user)
@@ -67,7 +71,7 @@ class ListLoadProcessor
$params = new Params();
}
foreach ($this->getLoaderList($entity->getEntityType()) as $processor) {
foreach ($this->getLoaderList($entity->getEntityType(), $params) as $processor) {
$processor->process($entity, $params);
}
}
@@ -75,7 +79,7 @@ class ListLoadProcessor
/**
* @return Loader<Entity>[]
*/
private function getLoaderList(string $entityType): array
private function getLoaderList(string $entityType, Params $params): array
{
if (array_key_exists($entityType, $this->loaderListMapCache)) {
return $this->loaderListMapCache[$entityType];
@@ -83,7 +87,7 @@ class ListLoadProcessor
$list = [];
foreach ($this->getLoaderClassNameList($entityType) as $className) {
foreach ($this->getLoaderClassNameList($entityType, $params) as $className) {
$list[] = $this->createLoader($className);
}
@@ -95,15 +99,19 @@ class ListLoadProcessor
/**
* @return class-string<Loader<Entity>>[]
*/
private function getLoaderClassNameList(string $entityType): array
private function getLoaderClassNameList(string $entityType, Params $params): array
{
$entityLevelList = $this->getEntityLevelClassNameList($entityType, $params);
$list = $this->metadata
->get(['app', 'fieldProcessing', 'listLoaderClassNameList']) ?? [];
$additionalList = $this->metadata
->get(['recordDefs', $entityType, 'listLoaderClassNameList']) ?? [];
return array_merge($list, $additionalList);
$list = array_merge($entityLevelList, $list, $additionalList);
return array_values(array_unique($list));
}
/**
@@ -114,4 +122,42 @@ class ListLoadProcessor
{
return $this->injectableFactory->createWithBinding($className, $this->bindingContainer);
}
/**
* @return class-string<Loader<Entity>>[]
*/
private function getEntityLevelClassNameList(string $entityType, Params $params): array
{
$entityLevelList = [];
$fieldList = $this->defs->getEntity($entityType)->getFieldList();
foreach ($fieldList as $fieldDefs) {
$className = $fieldDefs->getParam('loaderClassName');
if (!$className || in_array($className, $entityLevelList)) {
continue;
}
if ($params->hasSelect()) {
$hasAttribute = false;
foreach ($this->fieldUtil->getAttributeList($entityType, $fieldDefs->getName()) as $attribute) {
if ($params->hasInSelect($attribute)) {
$hasAttribute = true;
break;
}
}
if (!$hasAttribute) {
continue;
}
}
$entityLevelList[] = $className;
}
return $entityLevelList;
}
}
@@ -33,6 +33,7 @@ use Espo\Core\Acl;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Entities\User;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\Core\FieldProcessing\Loader\Params;
@@ -53,7 +54,8 @@ class ReadLoadProcessor
private InjectableFactory $injectableFactory,
private Metadata $metadata,
private Acl $acl,
private User $user
private User $user,
private Defs $defs,
) {
$this->bindingContainer = BindingContainerBuilder::create()
->bindInstance(User::class, $this->user)
@@ -97,14 +99,18 @@ class ReadLoadProcessor
*/
private function getLoaderClassNameList(string $entityType): array
{
$entityLevelList = $this->getEntityLevelClassNameList($entityType);
$list = $this->metadata
->get(['app', 'fieldProcessing', 'readLoaderClassNameList']) ?? [];
$additionalList = $this->metadata
->get(['recordDefs', $entityType, 'readLoaderClassNameList']) ?? [];
/** @var class-string<Loader<Entity>>[] */
return array_merge($list, $additionalList);
/** @var class-string<Loader<Entity>>[] $list */
$list = array_merge($entityLevelList, $list, $additionalList);
return array_values(array_unique($list));
}
/**
@@ -115,4 +121,26 @@ class ReadLoadProcessor
{
return $this->injectableFactory->createWithBinding($className, $this->bindingContainer);
}
/**
* @return class-string<Loader<Entity>>[]
*/
private function getEntityLevelClassNameList(string $entityType): array
{
$entityLevelList = [];
$fieldList = $this->defs->getEntity($entityType)->getFieldList();
foreach ($fieldList as $fieldDefs) {
$className = $fieldDefs->getParam('loaderClassName');
if (!$className || in_array($className, $entityLevelList)) {
continue;
}
$entityLevelList[] = $className;
}
return $entityLevelList;
}
}
@@ -0,0 +1,68 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* 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\FieldProcessing;
use Espo\Core\InjectableFactory;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use RuntimeException;
/**
* @since 9.1.0
* @internal Yet experimental.
*/
class SpecificFieldLoader
{
public function __construct(
private Defs $defs,
private InjectableFactory $injectableFactory,
) {}
public function process(Entity $entity, string $field): void
{
/** @var ?class-string<Loader<Entity>> $loaderClassName */
$loaderClassName = $this->defs
->getEntity($entity->getEntityType())
->tryGetField($field)
?->getParam('loaderClassName');
if (!$loaderClassName) {
return;
}
$loader = $this->injectableFactory->create($loaderClassName);
if (!$loader instanceof Loader) {
throw new RuntimeException("Bad field loader.");
}
$loader->process($entity, Loader\Params::create());
}
}
@@ -29,7 +29,9 @@
namespace Espo\Core\Formula;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\Utils\FieldUtil;
use Espo\Entities\EmailAddress;
use Espo\Entities\PhoneNumber;
use Espo\ORM\Defs\Params\AttributeParam as OrmAttributeParam;
@@ -47,7 +49,11 @@ class AttributeFetcher
/** @var array<string, mixed> */
private $relatedEntitiesCacheMap = [];
public function __construct(private EntityManager $entityManager) {}
public function __construct(
private EntityManager $entityManager,
private FieldUtil $fieldUtil,
private SpecificFieldLoader $specificFieldLoader,
) {}
public function fetch(Entity $entity, string $attribute, bool $getFetchedAttribute = false): mixed
{
@@ -159,6 +165,14 @@ class AttributeFetcher
return;
}
$field = $this->fieldUtil->getFieldOfAttribute($entity->getEntityType(), $attribute);
if (!$field) {
return;
}
$this->specificFieldLoader->process($entity, $field);
}
public function resetRuntimeCache(): void
+16
View File
@@ -289,4 +289,20 @@ class FieldUtil
return $attributeList;
}
/**
* Get a field an attribute belongs to.
*
* @since 9.1.0
*/
public function getFieldOfAttribute(string $entityType, string $attribute): ?string
{
foreach ($this->getEntityTypeFieldList($entityType) as $field) {
if (in_array($attribute, $this->getAttributeList($entityType, $field))) {
return $field;
}
}
return null;
}
}
@@ -93,7 +93,8 @@
"detail",
"filters"
],
"massUpdateDisabled": true
"massUpdateDisabled": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader"
},
"to": {
"type": "varchar",
@@ -112,7 +113,8 @@
"detail",
"filters"
],
"massUpdateDisabled": true
"massUpdateDisabled": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader"
},
"cc": {
"type": "varchar",
@@ -129,7 +131,8 @@
"detail",
"filters"
],
"massUpdateDisabled": true
"massUpdateDisabled": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader"
},
"bcc": {
"type": "varchar",
@@ -145,7 +148,8 @@
"layoutAvailabilityList": [
"detail"
],
"massUpdateDisabled": true
"massUpdateDisabled": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader"
},
"replyTo": {
"type": "varchar",
@@ -157,7 +161,8 @@
"layoutAvailabilityList": [
"detail"
],
"massUpdateDisabled": true
"massUpdateDisabled": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader"
},
"personStringData": {
"type": "varchar",
@@ -11,7 +11,8 @@
"url": {
"type": "url",
"notStorable": true,
"readOnly": true
"readOnly": true,
"loaderClassName": "Espo\\Classes\\FieldProcessing\\Portal\\UrlLoader"
},
"customId": {
"type": "varchar",
@@ -1,10 +1,4 @@
{
"readLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\Portal\\UrlLoader"
],
"listLoaderClassNameList": [
"Espo\\Classes\\FieldProcessing\\Portal\\UrlLoader"
],
"massActions": {
"delete": {
"allowed": true
+4
View File
@@ -1057,6 +1057,10 @@
"type": "string",
"description": "Converts field defs to ORM metadata. Should implement Espo\\Core\\Utils\\Database\\Orm\\FieldConverter."
},
"loaderClassName": {
"type": "string",
"description": "Loader. Must implement Espo\\Core\\FieldProcessing\\Loader. As of v9.1."
},
"duplicateIgnore": {
"type": "boolean",
"description": "Ignore when duplicating a record."
@@ -29,12 +29,15 @@
namespace tests\unit\Espo\Core\Formula;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\Formula\Evaluator;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\UndefinedKey;
use Espo\Core\Formula\Exceptions\UnsafeFunction;
use Espo\Core\InjectableFactory;
use Espo\Core\Formula\Exceptions\SyntaxError;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\Log;
use Espo\ORM\EntityManager;
@@ -58,9 +61,14 @@ class EvaluatorTest extends TestCase
$container = $containerMocker->create([
'log' => $log,
'entityManager' => $entityManager,
'fieldUtil' => $this->createMock(FieldUtil::class),
]);
$injectableFactory = new InjectableFactory($container);
$bindingContainer = BindingContainerBuilder::create()
->bindInstance(SpecificFieldLoader::class, $this->createMock(SpecificFieldLoader::class))
->build();
$injectableFactory = new InjectableFactory($container, $bindingContainer);
$this->evaluator = new Evaluator($injectableFactory, [], ['test\\unsafe']);
}
+6 -1
View File
@@ -30,6 +30,7 @@
namespace tests\unit\Espo\Core\Formula;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\Formula\AttributeFetcher;
use Espo\Core\Formula\Parser\Ast\Attribute;
use Espo\Core\Formula\Parser\Ast\Node;
@@ -38,6 +39,7 @@ use Espo\Core\Formula\Parser\Ast\Variable;
use Espo\Core\Formula\Processor;
use Espo\Core\Formula\Argument;
use Espo\Core\Utils\DateTime;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\NumberUtil;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
@@ -142,7 +144,10 @@ class FormulaTest extends TestCase
->build()
);
$attributeFetcher = new AttributeFetcher($this->entityManager);
$fieldUtil = $this->createMock(FieldUtil::class);
$loader = $this->createMock(SpecificFieldLoader::class);
$attributeFetcher = new AttributeFetcher($this->entityManager, $fieldUtil, $loader);
return new Processor(
$injectableFactory,