dynamic logic required backend
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user