*/ private $checkerCache = []; /** @var array> */ private $validatorCache = []; private Metadata $metadata; private FieldUtil $fieldUtil; private CheckerFactory $checkerFactory; private ValidatorFactory $validatorFactory; public function __construct( Metadata $metadata, FieldUtil $fieldUtil, CheckerFactory $factory, ValidatorFactory $validatorFactory ) { $this->metadata = $metadata; $this->fieldUtil = $fieldUtil; $this->checkerFactory = $factory; $this->validatorFactory = $validatorFactory; } /** * Process validation. * * @param Entity $entity An entity. * @param ?stdClass $data Raw request payload data. * @param ?FieldValidationParams $params Validation additional parameters. * * @throws ValidationError On the first invalid check. */ public function process(Entity $entity, ?stdClass $data = null, ?FieldValidationParams $params = null): void { $this->processInternal($entity, $data, $params, true); } /** * Process validation w/o exception throwing. * * @param Entity $entity An entity. * @param ?stdClass $data Raw request payload data. * @param ?FieldValidationParams $params Validation additional parameters. * @return Failure[] A list of validation failures. */ public function processAll(Entity $entity, ?stdClass $data = null, ?FieldValidationParams $params = null): array { try { return $this->processInternal($entity, $data, $params, false); } catch (ValidationError $e) { throw new LogicException(); } } /** * @return Failure[] * @throws ValidationError On the first invalid check. */ private function processInternal( Entity $entity, ?stdClass $data, ?FieldValidationParams $params, bool $throw ): array { $dataIsSet = $data !== null; if (!$data) { $data = (object) []; } if (!$params) { $params = new FieldValidationParams(); } $fieldList = $this->fieldUtil->getEntityTypeFieldList($entity->getEntityType()); $skipFieldList = $params->getSkipFieldList(); $failureList = []; foreach ($fieldList as $field) { if (in_array($field, $skipFieldList)) { continue; } if ( !$entity->isNew() && $dataIsSet && !$this->isFieldSetInData($entity->getEntityType(), $field, $data) ) { continue; } $itemFailureList = $this->processField($entity, $field, $params, $data, $throw); $failureList = array_merge($failureList, $itemFailureList); } return $failureList; } /** * Check a specific field for a specific validation type. */ public function check(Entity $entity, string $field, string $type, ?stdClass $data = null): bool { if (!$data) { $data = (object) []; } $entityType = $entity->getEntityType(); $fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type'); $validationValue = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, $type); $mandatoryValidationList = $this->metadata->get(['fields', $fieldType, 'mandatoryValidationList'], []); if (!in_array($type, $mandatoryValidationList)) { if (is_null($validationValue) || $validationValue === false) { return true; } } $result1 = $this->processFieldCheck($entityType, $type, $entity, $field, $validationValue); if (!$result1) { return false; } $result2 = $this->processFieldRawCheck($entityType, $type, $data, $field, $validationValue); if (!$result2) { return false; } $result3 = $this->processValidator($entity, $field, $type, new Data($data)); if (!$result3) { return false; } return true; } private function processValidator(Entity $entity, string $field, string $type, Data $data): bool { $validator = $this->getValidator($entity->getEntityType(), $field, $type); if (!$validator) { return true; } $failure = $validator->validate($entity, $field, $data); if ($failure) { return false; } return true; } /** * @return ?Validator */ private function getValidator(string $entityType, string $field, string $type): ?Validator { $key = $entityType . '_' . $field . '_' . $type; if (array_key_exists($key, $this->validatorCache)) { return $this->validatorCache[$key]; } if (!$this->validatorFactory->isCreatable($entityType, $field, $type)) { $this->validatorCache[$key] = null; return null; } $validator = $this->validatorFactory->create($entityType, $field, $type); $this->validatorCache[$key] = $validator; return $validator; } /** * @return Failure[] * @throws ValidationError */ private function processField( Entity $entity, string $field, FieldValidationParams $params, stdClass $data, bool $throw ): array { $entityType = $entity->getEntityType(); $fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type'); $validationList = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'validationList']) ?? $this->metadata->get(['fields', $fieldType, 'validationList']) ?? []; $mandatoryValidationList = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'mandatoryValidationList']) ?? $this->metadata->get(['fields', $fieldType, 'mandatoryValidationList']) ?? []; $validationList = array_unique(array_merge($validationList, $mandatoryValidationList)); $failureList = []; foreach ($validationList as $type) { $value = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, $type); if (is_null($value) && !in_array($type, $mandatoryValidationList)) { continue; } if (in_array($field, $params->getTypeSkipFieldList($type))) { continue; } $result = $this->check($entity, $field, $type, $data); if ($result) { continue; } $failure = new Failure($entityType, $field, $type); $failureList[] = $failure; if ($throw) { throw ValidationError::create($failure); } } $additionalFailureList = $this->checkAdditional($entity, $field, new Data($data)); if ($throw && $additionalFailureList !== []) { throw ValidationError::create($additionalFailureList[0]); } return array_merge($failureList, $additionalFailureList); } /** * @param mixed $validationValue */ private function processFieldCheck( string $entityType, string $type, Entity $entity, string $field, $validationValue ): bool { $checker = $this->getFieldTypeChecker($entityType, $field); if (!$checker) { return true; } $methodName = 'check' . ucfirst($type); if (!method_exists($checker, $methodName)) { return true; } return $checker->$methodName($entity, $field, $validationValue); } /** * @param mixed $validationValue */ private function processFieldRawCheck( string $entityType, string $type, stdClass $data, string $field, $validationValue ): bool { $checker = $this->getFieldTypeChecker($entityType, $field); if (!$checker) { return true; } $methodName = 'rawCheck' . ucfirst($type); if (!method_exists($checker, $methodName)) { return true; } return $checker->$methodName($data, $field, $validationValue); } private function getFieldTypeChecker(string $entityType, string $field): ?object { $key = $entityType . '_' . $field; if (!array_key_exists($key, $this->checkerCache)) { $this->loadFieldTypeChecker($entityType, $field); } return $this->checkerCache[$key]; } private function loadFieldTypeChecker(string $entityType, string $field): void { $key = $entityType . '_' . $field; if (!$this->checkerFactory->isCreatable($entityType, $field)) { $this->checkerCache[$key] = null; return; } $this->checkerCache[$key] = $this->checkerFactory->create($entityType, $field); } private function isFieldSetInData(string $entityType, string $field, stdClass $data): bool { $attributeList = $this->fieldUtil->getActualAttributeList($entityType, $field); $isSet = false; foreach ($attributeList as $attribute) { if (property_exists($data, $attribute)) { $isSet = true; break; } } return $isSet; } /** * @return Failure[] */ private function checkAdditional(Entity $entity, string $field, Data $data): array { $validatorList = $this->validatorFactory->createAdditionalList($entity->getEntityType(), $field); $failureList = []; foreach ($validatorList as $validator) { $itemFailure = $validator->validate($entity, $field, $data); if (!$itemFailure) { continue; } $type = lcfirst((new ReflectionClass($validator))->getShortName()); $failureList[] = new Failure($entity->getEntityType(), $field, $type); } return $failureList; } }