<?php
declare(strict_types=1);
namespace Swag\EnterpriseSearch\Action\Validator;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
use Swag\EnterpriseSearch\Action\ActionDefinition;
use Swag\EnterpriseSearch\Action\ActionSearchTerm\ActionSearchTermDefinition;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
class ActionValidator implements EventSubscriberInterface
{
/**
* @var Connection
*/
private $connection;
/**
* @internal
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public static function getSubscribedEvents(): array
{
return [
PreWriteValidationEvent::class => 'preValidate',
];
}
public function preValidate(PreWriteValidationEvent $event): void
{
$writeCommands = $event->getCommands();
$writeActionCommands = array_filter($writeCommands, function ($command) {
return $command->getDefinition()->getEntityName() === ActionDefinition::ENTITY_NAME && ($command instanceof InsertCommand || $command instanceof UpdateCommand);
});
$writeSearchTermsCommands = array_filter($writeCommands, function ($command) {
return $command->getDefinition()->getEntityName() === ActionSearchTermDefinition::ENTITY_NAME && ($command instanceof InsertCommand || $command instanceof UpdateCommand);
});
if (empty($writeActionCommands) && $writeSearchTermsCommands) {
return;
}
$violationList = new ConstraintViolationList();
if (count($writeActionCommands) > 0) {
$this->validateActions($writeActionCommands, $violationList);
}
if (count($writeSearchTermsCommands) > 0) {
$this->validateActionSearchTerms($writeActionCommands, $writeSearchTermsCommands, $violationList);
}
if ($violationList->count() > 0) {
$event->getExceptions()->add(new WriteConstraintViolationException($violationList));
}
}
private function getActions(array $actionIds): array
{
$actions = $this->connection->fetchAll(
'SELECT * FROM `action` WHERE `id` IN (:ids)',
['ids' => $actionIds],
['ids' => Connection::PARAM_STR_ARRAY]
);
$result = [];
foreach ($actions as $action) {
$result[$action['id']] = $action;
}
return $result;
}
private function validateActions(array $commands, ConstraintViolationList $violationList): void
{
$actionIds = [];
$payload = [];
$actionNames = [];
$usedActionNames = [];
foreach ($commands as $command) {
$id = $command->getPrimaryKey()['id'];
$actionIds[] = $id;
$payload[$id] = $command->getPayload();
if (isset($command->getPayload()['name'])) {
$actionNames[] = $command->getPayload()['name'];
}
}
$actions = $this->getActions($actionIds);
if (count($actionNames) > 0) {
$usedActionNames = $this->getUsedActionNames($actionNames);
}
foreach ($actionIds as $actionId) {
$action = $actions[$actionId] ?? [];
$action = array_replace_recursive($action, $payload[$actionId]);
$usedNames = $usedActionNames[$action['sales_channel_id']] ?? [];
$name = isset($action['name']) ? mb_strtolower($action['name']) : null;
if (isset($usedNames[$name]) && $usedNames[$name] !== $actionId) {
$violationList->add($this->buildViolation(
'Name "' . $name . '" already exists. Please provide a different name.',
$name,
'name',
'ACTION_DUPLICATE_NAME_VIOLATION',
0
));
}
$validFrom = $action['valid_from'] ?? null;
$validTo = $action['valid_to'] ?? null;
if ($validFrom === null || $validTo === null) {
continue;
}
$dateFrom = new \DateTime($validFrom);
$dateTo = new \DateTime($validTo);
if ($dateTo < $dateFrom) {
$violationList->add($this->buildViolation(
'Expiration Date of action must be after Start date of action',
$validTo,
'validTo',
'ACTION_VALID_TO_VIOLATION',
0
));
}
}
}
private function validateActionSearchTerms(array $actionCommands, array $searchTermsCommands, ConstraintViolationList $violationList): void
{
$actionPayloads = [];
$actionSearchTermPayloads = [];
$terms = [];
$actionIds = [];
foreach ($actionCommands as $command) {
$id = $command->getPrimaryKey()['id'];
$actionPayloads[$id] = $command->getPayload();
}
foreach ($searchTermsCommands as $command) {
$term = $command->getPayload()['term'] ?? null;
if (!$term) {
continue;
}
$terms[] = $term;
$actionIds[] = $command->getPayload()['action_id'];
$actionSearchTermPayloads[] = $command->getPayload();
}
if (count($terms) === 0) {
return;
}
$usedSearchTerms = $this->getUsedSearchTerms($terms);
$actions = $this->getActions($actionIds);
foreach ($actionSearchTermPayloads as $actionSearchTerm) {
$term = $actionSearchTerm['term'] ? mb_strtolower($actionSearchTerm['term']) : null;
if (!$term) {
continue;
}
$actionId = $actionSearchTerm['action_id'];
$actionDb = $actions[$actionId] ?? [];
$actionPayload = $actionPayloads[$actionId] ?? [];
$action = array_replace_recursive($actionDb, $actionPayload);
$salesChannelId = $action['sales_channel_id'];
$usedTerms = $usedSearchTerms[$salesChannelId] ?? [];
if (in_array($term, $usedTerms, true)) {
$violationList->add($this->buildViolation(
'Term "' . $term . '" already exists. Please provide a different term.',
$term,
'value',
'ACTION_SEARCH_TERM_DUPLICATE_TERM_VIOLATION',
1
));
}
}
}
private function buildViolation(string $message, $invalidValue, string $propertyPath, string $code, int $index): ConstraintViolationInterface
{
$formattedPath = "/{$index}/{$propertyPath}";
return new ConstraintViolation(
$message,
'',
[
'value' => $invalidValue,
],
$invalidValue,
$formattedPath,
$invalidValue,
null,
$code
);
}
private function getUsedActionNames(array $names): array
{
$actions = $this->connection->fetchAll(
'SELECT id, name, sales_channel_id
FROM action
WHERE name IN (:names)',
['names' => $names],
['names' => Connection::PARAM_STR_ARRAY]
);
$result = [];
foreach ($actions as $action) {
$result[$action['sales_channel_id']][mb_strtolower($action['name'])] = $action['id'];
}
return $result;
}
private function getUsedSearchTerms(array $terms): array
{
$searchTerms = $this->connection->fetchAll(
'SELECT action_search_term.term as term, action.sales_channel_id as salesChannelId
FROM action_search_term
JOIN action ON action_search_term.action_id = action.id
WHERE term IN (:terms)',
['terms' => $terms],
['terms' => Connection::PARAM_STR_ARRAY]
);
$result = [];
foreach ($searchTerms as $searchTerm) {
$result[$searchTerm['salesChannelId']][] = mb_strtolower($searchTerm['term']);
}
return $result;
}
}