+declare(strict_types=1);
+namespace App\Infrastructure\Validation\Request;
+use App\Domain\Core\Exceptions\ValidationException;
+use App\Infrastructure\Service\VendorPackages\Request\ValidateRequestInterface;
+use App\Infrastructure\Validation\Constraints\ConfirmFieldConstraint;
+use App\Infrastructure\Validation\Constraints\UniqueEntityFieldConstraint;
+use App\Infrastructure\Validation\RuleHandler\IdenticalRuleHandler;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Translation\Exception\InvalidArgumentException as TranslationInvalidArgumentException;
+use Symfony\Component\Validator\Constraints\Email;
+use Symfony\Component\Validator\Constraints\Length;
+use Symfony\Component\Validator\Constraints\NotBlank;
+use Symfony\Component\Validator\Constraints\NotNull;
+use Symfony\Component\Validator\Constraints\Optional;
+use Symfony\Component\Validator\Constraints\Type;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
+use Symfony\Component\Validator\Exception\InvalidOptionsException;
+use Symfony\Component\Validator\Exception\MissingOptionsException;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+ * @todo - fixme refactor
+abstract class AbstractValidationRequest implements ValidateRequestInterface {
+ private Request $request;
+ private ValidatorInterface $validator;
+ private TranslatorInterface $translator;
+ public function __construct(
+ ValidatorInterface $validator,
+ TranslatorInterface $translator
+ $this->request = $request;
+ $this->validator = $validator;
+ $this->translator = $translator;
+ public function request(): Request {
+ * @throws TranslationInvalidArgumentException
+ * @throws MissingOptionsException
+ * @throws InvalidOptionsException
+ * @throws ConstraintDefinitionException
+ * @throws ValidationException
+ protected function handleConstraints(array $validateFields): self {
+ $allValidationStatements = [];
+ foreach ($validateFields as $validationField => $rulesArray) {
+ $rules = $this->formatValidationRules($rulesArray);
+ $validationFieldRules = $this->loopExistingValidationRules($validationField, $rules);
+ // dump($validationFieldRules);
+ // dump(array_key_exists('required', $rules));
+ $isFieldOptional = false === \array_key_exists('required', $rules);
+ $isFieldRequired = true === \array_key_exists('required', $rules) &&
+ false === \array_key_exists('nullable', $rules);
+ // validation field strategy in case field optional or required
+ if (true === $isFieldOptional) {
+ $nullableClass = $this->availableValidationRules()['nullable'];
+ $allValidationStatements[$validationField] = new $nullableClass($validationFieldRules);
+ if (true === $isFieldRequired) {
+ $allValidationStatements[$validationField] = $validationFieldRules;
+ // $allValidationStatementsWithAdditionalFields = \array_merge(
+ // $allValidationStatements,
+ // $this->fulfillOptionalFields()
+ $fieldsToValidate = $this->clearRequestFromExtraFields(
+ $this->request->attributes->all(),
+ $this->request->request->all(),
+ $this->request->query->all()
+ $errors = $this->validator->validate(
+ new Collection($allValidationStatements)
+ // dump($allValidationStatementsWithAdditionalFields);
+ // dump($fieldsToValidate);
+ $errorsExist = \count($errors) > 0;
+ if (true === $errorsExist) {
+ throw new ValidationException(
+ $this->translator->trans(
+ $this->validationErrors($errors)
+ private function formatValidationRules(array $rules): array {
+ foreach ($rules as $rule => $value) {
+ if (true === \is_numeric($rule)) {
+ * @param string $validationField
+ * @param string[] $rules
+ private function loopExistingValidationRules(string $validationField, array $rules): array {
+ $locale = $this->request->getLocale() ?? $this->request->getDefaultLocale();
+ // dump($validationField);
+ $availableRules = $this->availableValidationRules();
+ $messagesTranslationFileName = 'validation';
+ $inputNamesTranslationFileName = 'input_names';
+ $attributesTranslationFileName = 'validation_attributes';
+ $validationFieldRules = [];
+ $localeCode = $this->request->getLocale() ?? $this->request->getDefaultLocale();
+ foreach ($rules as $rule => $value) {
+ if (false === \array_key_exists($rule, $availableRules)) {
+ // @todo - finish upgrading validation constraints
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $inputNamesTranslationFileName
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $inputNamesTranslationFileName
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $inputNamesTranslationFileName
+ // dump($this->request->get($validationField));
+ $validationFieldRules[] = (new $availableRules[$rule]($this->translator))(
+ '{{ value }}' => $this->request->get($validationField),
+ '{{ compared_value }}' => $rules[$rule],
+ // dump($validationFieldRules);
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'confirmFor' => $this->request->request->get(
+ $this->getConfirmationField($validationField)
+ (string)$this->request->query->get($this->getConfirmationField($validationField)),
+ // TODO - security vulnerability if we stringify directly query params?
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $this->getConfirmationField($validationField),
+ $inputNamesTranslationFileName
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $inputNamesTranslationFileName
+ 'entityFieldName' => $validationField,
+ 'entityClass' => $this->classToValidate(
+ 'entityFilterClass' => $this->classToValidate(
+ 'ignoreUuid' => $this->classToValidate(
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'message' => $this->simpleTrans(
+ $messagesTranslationFileName,
+ $inputNamesTranslationFileName
+ $min = $this->minAndMaxValuesFromValidationStringRules(
+ $validationFieldRulesWithValues,
+ $max = $this->minAndMaxValuesFromValidationStringRules(
+ $validationFieldRulesWithValues,
+ $validationFieldRules[] = new $availableRules[$rule](
+ 'minMessage' => $this->translator->trans(
+ '{{ inputName }}' => $this->translator->trans(
+ $inputNamesTranslationFileName,
+ '%value%' => $this->translator->transChoice(
+ $attributesTranslationFileName,
+ $messagesTranslationFileName,
+ 'maxMessage' => $this->translator->trans(
+ '{{ inputName }}' => $this->translator->trans(
+ $inputNamesTranslationFileName,
+ '%value%' => $this->translator->transChoice(
+ $attributesTranslationFileName,
+ $messagesTranslationFileName,
+ // do nothing, skip validation if rule does not exists
+ return $validationFieldRules;
+ * Bind rules to its it string presentation, name
+ private function availableValidationRules(): array {
+ 'required' => NotNull::class,
+ 'filled' => NotBlank::class,
+ 'nullable' => Optional::class,
+ 'string' => Type::class,
+ 'length' => Length::class,
+ 'email' => Email::class,
+ 'unique' => UniqueEntityFieldConstraint::class,
+ 'confirm' => ConfirmFieldConstraint::class,
+ 'identical' => IdenticalRuleHandler::class,
+ //TODO - add validations https://laravel.com/docs/5.6/validation#available-validation-rules
+ 'exists' => ConfirmFieldConstraint::class,
+ 'url' => ConfirmFieldConstraint::class,
+ 'date' => ConfirmFieldConstraint::class,
+ 'after_date' => ConfirmFieldConstraint::class,
+ 'equal' => EqualTo::class,
+ 'array' => ConfirmFieldConstraint::class,
+ 'numeric' => ConfirmFieldConstraint::class,
+ 'int' => ConfirmFieldConstraint::class,
+ 'boolean' => ConfirmFieldConstraint::class,
+ * translates message with attribute
+ * @throws TranslationInvalidArgumentException
+ private function simpleTrans(
+ string $translationFileName,
+ string $translationAttributeId,
+ string $attributeFileName,
+ string $localeCode = 'en' // TODO - should be returned from system settigns
+ return $this->translator->trans(
+ '%attribute%' => $this->translator->trans(
+ $translationAttributeId,
+ * Getting name of field we need to confirm
+ * @param string $validationField
+ private function getConfirmationField(string $validationField): string {
+ $confirmationWord = \strpbrk(
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ $confirmationStartPosition = \strpos(
+ $confirmationStartPosition
+ * Used for getting class for unique validation, or existence
+ private function classToValidate(string $rules): array {
+ $removeAllRulesBeforeUnique = \substr(
+ $getClassAndFilter = \explode(
+ $removeAllRulesBeforeUnique
+ $clearedClassFromUniqueRule = \substr(
+ ) + 1 // + 1 removes : dots
+ 'class' => $clearedClassFromUniqueRule,
+ 'filter' => $getClassAndFilter[1],
+ 'id' => $getClassAndFilter[2] ?? '', // TODO - or better use null?
+ * Getting min and max values for validation rule (between:min,max)
+ * @param array $fieldRulesWithValues
+ private function minAndMaxValuesFromValidationStringRules(array $fieldRulesWithValues, string $rule): array {
+ $defaultMin = 1; // minimal required chars
+ $defaultMax = 65535;// max required chars text type
+ foreach ($fieldRulesWithValues as $fieldRulesWithValue) {
+ $fieldMinAndMaxValues = \explode(
+ 'min' => (int)$fieldMinAndMaxValues[0] ?: $defaultMin,
+ 'max' => (int)$fieldMinAndMaxValues[1] ?: $defaultMax,
+// return null; // TODO - or throw exception?
+ private function clearRequestFromExtraFields(array $validateFields, array $allRequestFields): array {
+ foreach ($allRequestFields as $key => $value) {
+ if (false === \array_key_exists($key, $validateFields)) {
+ unset($allRequestFields[$key]);
+ return $allRequestFields;
+ * return simple array of error messages
+ * @param ConstraintViolationListInterface $errorsCollection
+ private function validationErrors(ConstraintViolationListInterface $errorsCollection): array {
+ foreach ($errorsCollection as $error) {
+ $errors[\trim($error->getPropertyPath(), '[]')] = $error->getMessage();
+ * Adds to all request fields, additional, optional fields
+ * @throws MissingOptionsException
+ * @throws InvalidOptionsException
+ * @throws ConstraintDefinitionException
+ private function fulfillOptionalFields(): array {
+ $additionalFields = [];
+ foreach ($this->request->request->all() as $field => $rules) {
+ $isAdditionalField = \in_array(
+ $this->optionalFields(),
+ if ($isAdditionalField) {
+ $additionalFields[$field] = [new NotBlank()];
+ return $additionalFields;
+ private function optionalFields(): array {