custom/plugins/SwagCustomizedProducts/src/Template/Aggregate/TemplateOption/TemplateOptionValidator.php line 84

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /*
  3.  * (c) shopware AG <info@shopware.com>
  4.  * For the full copyright and license information, please view the LICENSE
  5.  * file that was distributed with this source code.
  6.  */
  7. namespace Swag\CustomizedProducts\Template\Aggregate\TemplateOption;
  8. use Doctrine\DBAL\Connection;
  9. use Doctrine\DBAL\Driver\ResultStatement;
  10. use Shopware\Core\Defaults;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSet;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  17. use Shopware\Core\Framework\Uuid\Uuid;
  18. use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
  19. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Exception\OptionTypeClassNotFoundException;
  20. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\Checkbox;
  21. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\ColorSelect;
  22. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\Constraint\HexColor;
  23. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\ImageSelect;
  24. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\OptionTypeCollection;
  25. use Swag\CustomizedProducts\Template\Aggregate\TemplateOption\Type\OptionTypeInterface;
  26. use Swag\CustomizedProducts\Template\Aggregate\TemplateOptionValue\TemplateOptionValueDefinition;
  27. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  28. use Symfony\Component\Validator\Constraints\NotBlank;
  29. use Symfony\Component\Validator\ConstraintViolation;
  30. use Symfony\Component\Validator\ConstraintViolationInterface;
  31. use Symfony\Component\Validator\ConstraintViolationList;
  32. use Symfony\Component\Validator\Validator\ValidatorInterface;
  33. class TemplateOptionValidator implements EventSubscriberInterface
  34. {
  35.     /**
  36.      * @var OptionTypeCollection
  37.      */
  38.     private $typeCollection;
  39.     /**
  40.      * @var ValidatorInterface
  41.      */
  42.     private $validator;
  43.     /**
  44.      * @var Connection
  45.      */
  46.     private $connection;
  47.     public function __construct(
  48.         ValidatorInterface $validator,
  49.         OptionTypeCollection $typeCollection,
  50.         Connection $connection
  51.     ) {
  52.         $this->validator $validator;
  53.         $this->typeCollection $typeCollection;
  54.         $this->connection $connection;
  55.     }
  56.     public static function getSubscribedEvents(): array
  57.     {
  58.         return [
  59.             PreWriteValidationEvent::class => 'preValidate',
  60.             PostWriteValidationEvent::class => 'postValidate',
  61.         ];
  62.     }
  63.     public function preValidate(PreWriteValidationEvent $event): void
  64.     {
  65.         foreach ($event->getCommands() as $command) {
  66.             if (!($command instanceof UpdateCommand)
  67.                 || ($command->getDefinition()->getClass() !== TemplateOptionDefinition::class && $command->getDefinition()->getClass() !== TemplateOptionValueDefinition::class)
  68.             ) {
  69.                 continue;
  70.             }
  71.             $command->requestChangeSet();
  72.         }
  73.     }
  74.     public function postValidate(PostWriteValidationEvent $event): void
  75.     {
  76.         $violationList = new ConstraintViolationList();
  77.         foreach ($event->getCommands() as $key => $command) {
  78.             // Validate the color select option values
  79.             if (($command instanceof InsertCommand || $command instanceof UpdateCommand)
  80.                 && $command->getDefinition()->getClass() === TemplateOptionValueDefinition::class
  81.             ) {
  82.                 $type $this->getOptionTypeOfOptionValue($command);
  83.                 if ($type === null || ($type !== ColorSelect::NAME && $type !== ImageSelect::NAME)) {
  84.                     continue;
  85.                 }
  86.                 if ($type === ColorSelect::NAME) {
  87.                     $this->postValidateColorValue((string) $key$command$violationList);
  88.                 }
  89.                 if ($type === ImageSelect::NAME) {
  90.                     $this->postValidateImageSelectValue((string) $key$command$violationList);
  91.                 }
  92.                 if ($violationList->count() > 0) {
  93.                     $event->getExceptions()->add(new WriteConstraintViolationException($violationList));
  94.                 }
  95.                 continue;
  96.             }
  97.             if (!($command instanceof InsertCommand || $command instanceof UpdateCommand)
  98.                 || $command->getDefinition()->getClass() !== TemplateOptionDefinition::class
  99.             ) {
  100.                 continue;
  101.             }
  102.             $changeSet null;
  103.             if ($command instanceof UpdateCommand) {
  104.                 $changeSet $command->getChangeSet();
  105.             }
  106.             // If a versionized entity is written ignore all constraints
  107.             if (isset($command->getPayload()['version_id'])
  108.                 && Uuid::fromBytesToHex($command->getPayload()['version_id']) !== Defaults::LIVE_VERSION
  109.             ) {
  110.                 continue;
  111.             }
  112.             // If the first version commit gets inserted, we dont validate
  113.             if (isset($command->getPayload()['type_properties'])) {
  114.                 $typeProperties \json_decode($command->getPayload()['type_properties'], true);
  115.                 if (isset($typeProperties['optionAdd'])) {
  116.                     continue;
  117.                 }
  118.             }
  119.             $payload $command->getPayload();
  120.             $currentId Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
  121.             if ($changeSet !== null) {
  122.                 $payload $this->setTypeIfNotSetDuringUpdate($command$payload$changeSet);
  123.             }
  124.             if ($command instanceof InsertCommand && !isset($payload['position'])) {
  125.                 $violationList->add(
  126.                     $this->buildViolation(
  127.                         'The property "{{ fieldName }}" should be set.',
  128.                         ['{{ fieldName }}' => 'position'],
  129.                         null,
  130.                         \sprintf('%s/position'$currentId)
  131.                     )
  132.                 );
  133.                 continue;
  134.             }
  135.             /** @var string|null $type */
  136.             $type $payload['type'] ?? null;
  137.             if ($type === null || !$this->validType($type)) {
  138.                 $violationList->add(
  139.                     $this->buildViolation(
  140.                         'This "type" value (%value%) is invalid.',
  141.                         ['%value%' => $type ?? 'NULL'],
  142.                         null,
  143.                         \sprintf('%s/type'$currentId)
  144.                     )
  145.                 );
  146.                 continue;
  147.             }
  148.             $extractValue $this->extractValue($payload);
  149.             $basePath \sprintf('%s/typeProperties'$currentId);
  150.             if ($command instanceof InsertCommand && !\array_key_exists('type_properties'$payload)) {
  151.                 $violationList->add(
  152.                     $this->buildViolation(
  153.                         'The property "{{ fieldName }} should be set".',
  154.                         ['{{ fieldName }}' => 'typeProperties'],
  155.                         null,
  156.                         $basePath
  157.                     )
  158.                 );
  159.                 continue;
  160.             }
  161.             if (!empty($extractValue)) {
  162.                 $validations $this->getOptionType($type)->getConstraints();
  163.                 $violationList->addAll($this->validateConsistence($basePath$validations$extractValue));
  164.             }
  165.             if ($changeSet !== null) {
  166.                 $this->postValidatePriceOnUpdate($currentId$changeSet$violationList);
  167.             } else {
  168.                 $this->postValidatePriceOnInsert($payload$currentId$violationList);
  169.             }
  170.             if ($command instanceof UpdateCommand) {
  171.                 $type $this->getCommandOptionType($command);
  172.             }
  173.             if ($type !== Checkbox::NAME) {
  174.                 continue;
  175.             }
  176.             $this->validateCheckboxIsNotRequired($type$key$command$violationList);
  177.         }
  178.         if ($violationList->count() > 0) {
  179.             $event->getExceptions()->add(new WriteConstraintViolationException($violationList));
  180.         }
  181.     }
  182.     private function getOptionType(string $type): OptionTypeInterface
  183.     {
  184.         foreach ($this->typeCollection->getIterator() as $optionType) {
  185.             if ($optionType->getName() !== $type) {
  186.                 continue;
  187.             }
  188.             return $optionType;
  189.         }
  190.         throw new OptionTypeClassNotFoundException($type);
  191.     }
  192.     private function validType(string $type): bool
  193.     {
  194.         return \in_array($type$this->typeCollection->getNames(), true);
  195.     }
  196.     private function buildViolation(
  197.         string $messageTemplate,
  198.         array $parameters,
  199.         ?string $root null,
  200.         ?string $propertyPath null,
  201.         ?string $invalidValue null,
  202.         ?string $code null
  203.     ): ConstraintViolationInterface {
  204.         return new ConstraintViolation(
  205.             \str_replace(\array_keys($parameters), $parameters$messageTemplate),
  206.             $messageTemplate,
  207.             $parameters,
  208.             $root,
  209.             $propertyPath,
  210.             $invalidValue,
  211.             null,
  212.             $code
  213.         );
  214.     }
  215.     private function validateConsistence(string $basePath, array $fieldValidations, array $payload): ConstraintViolationList
  216.     {
  217.         $list = new ConstraintViolationList();
  218.         foreach ($fieldValidations as $fieldName => $validations) {
  219.             $currentPath \sprintf('%s/%s'$basePath$fieldName);
  220.             $list->addAll(
  221.                 $this->validator->startContext()
  222.                     ->atPath($currentPath)
  223.                     ->validate($payload[$fieldName] ?? null$validations)
  224.                     ->getViolations()
  225.             );
  226.         }
  227.         foreach ($payload as $fieldName => $_value) {
  228.             $currentPath \sprintf('%s/%s'$basePath$fieldName);
  229.             if (!\array_key_exists($fieldName$fieldValidations) && $fieldName !== '_name') {
  230.                 $list->add(
  231.                     $this->buildViolation(
  232.                         'The property "{{ fieldName }}" is not allowed.',
  233.                         ['{{ fieldName }}' => $fieldName],
  234.                         null,
  235.                         $currentPath
  236.                     )
  237.                 );
  238.             }
  239.         }
  240.         return $list;
  241.     }
  242.     private function extractValue(array $payload): array
  243.     {
  244.         if (!\array_key_exists('type_properties'$payload) || $payload['type_properties'] === null) {
  245.             return [];
  246.         }
  247.         return \json_decode($payload['type_properties'], true);
  248.     }
  249.     private function setTypeIfNotSetDuringUpdate(WriteCommand $command, array $payloadChangeSet $changeSet): array
  250.     {
  251.         if (!($command instanceof UpdateCommand) || $changeSet->hasChanged('type') || $changeSet->getBefore('type') === null) {
  252.             return $payload;
  253.         }
  254.         $payload['type'] = $changeSet->getBefore('type');
  255.         return $payload;
  256.     }
  257.     private function postValidatePriceOnInsert(array $payloadstring $currentIdConstraintViolationList $violationList): void
  258.     {
  259.         if (isset($payload['relative_surcharge']) || isset($payload['price']) || isset($payload['percentage_surcharge'])) {
  260.             $relative_surcharge = (bool) ($payload['relative_surcharge'] ?? false);
  261.             $price $payload['price'] ?? null;
  262.             $percentage_surcharge $payload['percentage_surcharge'] ?? null;
  263.             $this->postValidatePrice($currentId$relative_surcharge$price !== null$percentage_surcharge !== null$violationList);
  264.         }
  265.     }
  266.     private function postValidatePriceOnUpdate(string $currentIdChangeSet $changeSetConstraintViolationList $violationList): void
  267.     {
  268.         if ($changeSet->hasChanged('relative_surcharge') || $changeSet->hasChanged('price') || $changeSet->hasChanged('percentage_surcharge')) {
  269.             $relative_surcharge = (bool) ($changeSet->getAfter('relative_surcharge') ?? $changeSet->hasChanged('relative_surcharge'));
  270.             $price $changeSet->getAfter('price') ?? $changeSet->getBefore('price');
  271.             $percentage_surcharge $changeSet->getAfter('percentage_surcharge') ?? $changeSet->getBefore('percentage_surcharge');
  272.             $this->postValidatePrice($currentId$relative_surcharge$price !== null$percentage_surcharge !== null$violationList);
  273.         }
  274.     }
  275.     private function postValidatePrice(
  276.         string $currentId,
  277.         bool $relativeSurcharge,
  278.         bool $hasPrice,
  279.         bool $hasPercentageSurcharge,
  280.         ConstraintViolationList $violationList
  281.     ): void {
  282.         if ($relativeSurcharge && !$hasPercentageSurcharge) {
  283.             $violationList->add(
  284.                 $this->buildViolation(
  285.                     'The property "{{ fieldName }}" should be set.',
  286.                     ['{{ fieldName }}' => 'percentageSurcharge'],
  287.                     null,
  288.                     \sprintf('%s/percentageSurcharge'$currentId)
  289.                 )
  290.             );
  291.             return;
  292.         }
  293.         if (!$relativeSurcharge && !$hasPrice) {
  294.             $violationList->add(
  295.                 $this->buildViolation(
  296.                     'The property "{{ fieldName }}" should be set.',
  297.                     ['{{ fieldName }}' => 'price'],
  298.                     null,
  299.                     \sprintf('%s/price'$currentId)
  300.                 )
  301.             );
  302.             return;
  303.         }
  304.     }
  305.     private function postValidateColorValue(string $commandKeyWriteCommand $commandConstraintViolationList $violationList): void
  306.     {
  307.         $value = [];
  308.         if ($command instanceof UpdateCommand) {
  309.             $changeSet $command->getChangeSet();
  310.             if ($changeSet === null || !$changeSet->hasChanged('value')) {
  311.                 return;
  312.             }
  313.             $afterValue $changeSet->getAfter('value');
  314.             if (!\is_string($afterValue)) {
  315.                 return;
  316.             }
  317.             $value \json_decode($afterValuetrue);
  318.         }
  319.         if ($command instanceof InsertCommand) {
  320.             $payload $command->getPayload();
  321.             if (isset($payload['value'])) {
  322.                 $value \json_decode($payload['value'], true);
  323.             }
  324.         }
  325.         $constraints = [new HexColor(), new NotBlank()];
  326.         $violationList->addAll(
  327.             $this->validator->startContext()
  328.                 ->atPath(\sprintf('%s/value/_value'$commandKey))
  329.                 ->validate($value['_value'] ?? null$constraints)
  330.                 ->getViolations()
  331.         );
  332.     }
  333.     private function postValidateImageSelectValue(string $commandKeyWriteCommand $commandConstraintViolationList $violationList): void
  334.     {
  335.         $value = [];
  336.         if ($command instanceof UpdateCommand) {
  337.             $changeSet $command->getChangeSet();
  338.             if ($changeSet === null || !$changeSet->hasChanged('value')) {
  339.                 return;
  340.             }
  341.             $afterValue $changeSet->getAfter('value');
  342.             if (!\is_string($afterValue)) {
  343.                 return;
  344.             }
  345.             $value \json_decode($afterValuetrue);
  346.         }
  347.         if ($command instanceof InsertCommand) {
  348.             $payload $command->getPayload();
  349.             if (isset($payload['value'])) {
  350.                 $value \json_decode($payload['value'], true);
  351.             }
  352.         }
  353.         $constraints = [new NotBlank()];
  354.         $violationList->addAll(
  355.             $this->validator->startContext()
  356.                 ->atPath(\sprintf('%s/value/_value'$commandKey))
  357.                 ->validate($value['_value'] ?? null$constraints)
  358.                 ->getViolations()
  359.         );
  360.     }
  361.     private function getOptionTypeOfOptionValue(WriteCommand $command): ?string
  362.     {
  363.         $optionId null;
  364.         if ($command instanceof InsertCommand) {
  365.             $payload $command->getPayload();
  366.             if (!isset($payload['template_option_id'])) {
  367.                 return null;
  368.             }
  369.             $optionId $payload['template_option_id'];
  370.         }
  371.         if ($command instanceof UpdateCommand) {
  372.             $changeSet $command->getChangeSet();
  373.             if ($changeSet === null) {
  374.                 return null;
  375.             }
  376.             $optionId $changeSet->getBefore('template_option_id');
  377.         }
  378.         if ($optionId === null) {
  379.             return null;
  380.         }
  381.         $query $this->connection->createQueryBuilder()
  382.             ->select('type')
  383.             ->from('swag_customized_products_template_option')
  384.             ->where('id = :id')
  385.             ->setParameter('id'$optionId)
  386.             ->setMaxResults(1)
  387.             ->execute();
  388.         if (!($query instanceof ResultStatement)) {
  389.             return null;
  390.         }
  391.         return $query->fetchOne();
  392.     }
  393.     private function getCommandOptionType(UpdateCommand $command): ?string
  394.     {
  395.         $changeSet $command->getChangeSet();
  396.         if ($changeSet === null) {
  397.             return null;
  398.         }
  399.         $before $changeSet->getBefore('type');
  400.         if (!\is_string($before)) {
  401.             return null;
  402.         }
  403.         $after $changeSet->getAfter('type');
  404.         if (\is_string($after) && $after !== $before) {
  405.             return $after;
  406.         }
  407.         return $before;
  408.     }
  409.     private function validateCheckboxIsNotRequired(
  410.         string $type,
  411.         int $key,
  412.         WriteCommand $command,
  413.         ConstraintViolationList $violationList
  414.     ): void {
  415.         if ($command instanceof UpdateCommand) {
  416.             $changeSet $command->getChangeSet();
  417.             if ($changeSet === null) {
  418.                 return;
  419.             }
  420.             $beforeValue = (bool) $changeSet->getBefore('required');
  421.             $afterValue $changeSet->getAfter('required');
  422.             if (
  423.                 ($beforeValue === false && $afterValue === null)
  424.                 || ($afterValue !== null && ((bool) $afterValue) === false)
  425.             ) {
  426.                 return;
  427.             }
  428.         } else {
  429.             $payload $command->getPayload();
  430.             if (!\array_key_exists('required'$payload) || !$payload['required']) {
  431.                 return;
  432.             }
  433.         }
  434.         $violationList->add(
  435.             $this->buildViolation(
  436.                 'The property "{{ property }}" is prohibited for options of the type "{{ type }}".',
  437.                 [
  438.                     '{{ property }}' => 'required',
  439.                     '{{ type }}' => $type,
  440.                 ],
  441.                 null,
  442.                 \sprintf('%s/required'$key)
  443.             )
  444.         );
  445.     }
  446. }