entity defs field loaders
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user