dynamic logic required backend

This commit is contained in:
Yuri Kuznetsov
2025-04-08 10:33:57 +03:00
parent f4c6c16df7
commit 034a0b8ff1
4 changed files with 184 additions and 19 deletions
@@ -29,14 +29,16 @@
namespace Espo\Core\FieldValidation;
use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
use Espo\Core\Utils\Log;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\Core\FieldValidation\Exceptions\ValidationError;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\FieldUtil;
use Espo\Tools\DynamicLogic\ConditionCheckerFactory;
use Espo\Tools\DynamicLogic\Exceptions\BadCondition;
use Espo\Tools\DynamicLogic\Item as LogicItem;
use LogicException;
use stdClass;
@@ -59,7 +61,9 @@ class FieldValidationManager
private FieldUtil $fieldUtil,
CheckerFactory $factory,
private ValidatorFactory $validatorFactory,
private Defs $defs
private Defs $defs,
private ConditionCheckerFactory $conditionCheckerFactory,
private Log $log,
) {
$this->checkerFactory = $factory;
}
@@ -107,30 +111,38 @@ class FieldValidationManager
): array {
$dataIsSet = $data !== null;
$entityType = $entity->getEntityType();
$data ??= (object) [];
$params ??= new FieldValidationParams();
$fieldList = array_filter(
$this->fieldUtil->getEntityTypeFieldList($entity->getEntityType()),
fn ($field) => !in_array($field, $params->getSkipFieldList())
);
$failureList = [];
$entityDefs = $this->defs->getEntity($entity->getEntityType());
$fieldList = $this->getFieldList($entityType, $params);
$entityDefs = $this->defs->getEntity($entityType);
foreach ($fieldList as $field) {
$typeList = null;
if (
!$entity->isNew() &&
$dataIsSet &&
!$entityDefs->tryGetField($field)?->getParam('forceValidation') &&
!$this->isFieldSetInData($entity->getEntityType(), $field, $data)
!$this->isFieldSetInData($entityType, $field, $data)
) {
continue;
if (!$this->hasRequiredLogic($entityType, $field)) {
continue;
}
$typeList = [Type::REQUIRED];
}
$itemFailureList = $this->processField($entity, $field, $params, $data, $throw);
$itemFailureList = $this->processField(
entity: $entity,
field: $field,
params: $params,
data: $data,
throw: $throw,
typeList: $typeList,
);
$failureList = array_merge($failureList, $itemFailureList);
}
@@ -193,6 +205,14 @@ class FieldValidationManager
$validationValue = $value ?? $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, $type);
$isMandatory = in_array($type, $this->getMandatoryValidationList($entityType, $field));
if (
$type === Type::REQUIRED &&
!$validationValue &&
$this->checkDynamicLogicRequired($entity, $field)
) {
$validationValue = true;
}
$skip = !$isMandatory && (is_null($validationValue) || $validationValue === false);
if ($skip) {
@@ -256,6 +276,7 @@ class FieldValidationManager
}
/**
* @param ?string[] $typeList
* @return Failure[]
* @throws ValidationError
*/
@@ -264,12 +285,17 @@ class FieldValidationManager
string $field,
FieldValidationParams $params,
stdClass $data,
bool $throw
bool $throw,
?array $typeList = null,
): array {
$validationList = $this->getAllValidationList($entity->getEntityType(), $field, $params);
foreach ($validationList as $type) {
if ($typeList !== null && !in_array($type, $typeList)) {
continue;
}
$result = $this->check($entity, $field, $type, $data);
if ($result) {
@@ -435,4 +461,44 @@ class FieldValidationManager
return null;
}
private function checkDynamicLogicRequired(Entity $entity, string $field): bool
{
$entityType = $entity->getEntityType();
/** @var stdClass[] $group */
$group = $this->metadata->getObjects("logicDefs.$entityType.fields.$field.required.conditionGroup");
if (!is_array($group)) {
return false;
}
$checker = $this->conditionCheckerFactory->create($entity);
try {
$item = LogicItem::fromGroupDefinition($group);
return $checker->check($item);
} catch (BadCondition $e) {
$this->log->warning("Bad logic condition for $entityType $field.", ['exception' => $e]);
}
return false;
}
/**
* @return string[]
*/
private function getFieldList(string $entityType, FieldValidationParams $params): array
{
return array_filter(
$this->fieldUtil->getEntityTypeFieldList($entityType),
fn($field) => !in_array($field, $params->getSkipFieldList())
);
}
private function hasRequiredLogic(string $entityType, string $field): bool
{
return (bool) $this->metadata->get("logicDefs.$entityType.fields.$field.required");
}
}
@@ -0,0 +1,38 @@
<?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\FieldValidation;
/**
* @since 9.1.0
*/
class Type
{
public const REQUIRED = 'required';
}
@@ -32,7 +32,8 @@ namespace Espo\Tools\LeadCapture;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Field\Link;
use Espo\Core\FieldValidation\Exceptions\ValidationError;
use Espo\Core\FieldValidation\Failure;
use Espo\Core\FieldValidation\Failure as ValidationFailure;
use Espo\Core\FieldValidation\Type as ValidationType;
use Espo\Core\Name\Field;
use Espo\Core\PhoneNumber\Sanitizer as PhoneNumberSanitizer;
use Espo\Core\Job\JobSchedulerFactory;
@@ -511,7 +512,7 @@ class CaptureService
$lead->addLinkMultipleId(Field::TEAMS, $leadCapture->getTargetTeamId());
}
$validationParams = ValidationParams::create()->withTypeSkipFieldList('required', $fieldList);
$validationParams = ValidationParams::create()->withTypeSkipFieldList(ValidationType::REQUIRED, $fieldList);
$this->fieldValidationManager->process($lead, $data, $validationParams);
@@ -520,10 +521,10 @@ class CaptureService
continue;
}
$notValid = $this->fieldValidationManager->check($lead, $field, 'required', $data, true);
$notValid = $this->fieldValidationManager->check($lead, $field, ValidationType::REQUIRED, $data, true);
if (!$notValid) {
$failure = new Failure(Lead::ENTITY_TYPE, $field, 'required');
$failure = new ValidationFailure(Lead::ENTITY_TYPE, $field, ValidationType::REQUIRED);
throw ValidationError::create($failure);
}
@@ -31,6 +31,7 @@ namespace tests\integration\Espo\Record;
use Espo\Core\Application;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\FieldValidation\Type;
use Espo\Core\Record\CreateParams;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Record\UpdateParams;
@@ -515,4 +516,63 @@ class FieldValidationTest extends BaseTestCase
$this->assertTrue($thrown);
}
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testDynamicLogicRequired(): void
{
$metadata = $this->getMetadata();
$metadata->set('logicDefs', Account::ENTITY_TYPE, [
'fields' => [
'description' => [
Type::REQUIRED => [
'conditionGroup' => [
[
'type' => 'equals',
'attribute' => 'type',
'value' => 'Customer',
]
]
]
]
]
]);
$metadata->save();
$this->reCreateApplication();
$service = $this->getContainer()->getByClass(ServiceContainer::class)->getByClass(Account::class);
$account = $service->create((object) [
'name' => 'Test',
], CreateParams::create());
$isThrown = false;
try {
$service->update($account->getId(), (object) [
'type' => 'Customer',
], UpdateParams::create());
} catch (BadRequest) {
$isThrown = true;
}
$this->assertTrue($isThrown);
$isThrown = false;
try {
$service->update($account->getId(), (object) [
'type' => 'Customer',
'description' => 'Test.',
], UpdateParams::create());
} catch (BadRequest) {
$isThrown = true;
}
$this->assertFalse($isThrown);
}
}