Passed
Push — trunk ( d9cd06...1ca626 )
by Christian
16:06 queued 03:18
created

RuleAreaUpdater::addFlowConditionSelect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 9
rs 10
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Content\Rule\DataAbstractionLayer;
4
5
use Doctrine\DBAL\Connection;
6
use Shopware\Core\Checkout\Cart\CachedRuleLoader;
7
use Shopware\Core\Content\Rule\RuleDefinition;
8
use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
9
use Shopware\Core\Framework\DataAbstractionLayer\CompiledFieldCollection;
10
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
11
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder;
12
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
13
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
14
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
15
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
16
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
17
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
18
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
19
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas;
20
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
21
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
22
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
23
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
24
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
25
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
26
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
27
use Shopware\Core\Framework\Rule\Collector\RuleConditionRegistry;
28
use Shopware\Core\Framework\Uuid\Uuid;
29
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
30
31
/**
32
 * @internal
33
 */
34
class RuleAreaUpdater implements EventSubscriberInterface
35
{
36
    private Connection $connection;
37
38
    private RuleDefinition $definition;
39
40
    private RuleConditionRegistry $conditionRegistry;
41
42
    private CacheInvalidator $cacheInvalidator;
43
44
    /**
45
     * @internal
46
     */
47
    public function __construct(
48
        Connection $connection,
49
        RuleDefinition $definition,
50
        RuleConditionRegistry $conditionRegistry,
51
        CacheInvalidator $cacheInvalidator
52
    ) {
53
        $this->connection = $connection;
54
        $this->definition = $definition;
55
        $this->conditionRegistry = $conditionRegistry;
56
        $this->cacheInvalidator = $cacheInvalidator;
57
    }
58
59
    public static function getSubscribedEvents(): array
60
    {
61
        return [
62
            PreWriteValidationEvent::class => 'triggerChangeSet',
63
            EntityWrittenContainerEvent::class => 'onEntityWritten',
64
        ];
65
    }
66
67
    public function triggerChangeSet(PreWriteValidationEvent $event): void
68
    {
69
        $associatedEntities = $this->getAssociationEntities();
70
71
        foreach ($event->getCommands() as $command) {
72
            $definition = $command->getDefinition();
73
            $entity = $definition->getEntityName();
74
75
            if (!$command instanceof ChangeSetAware || !\in_array($entity, $associatedEntities, true)) {
76
                continue;
77
            }
78
79
            if ($command instanceof DeleteCommand) {
80
                $command->requestChangeSet();
81
82
                continue;
83
            }
84
85
            foreach ($this->getForeignKeyFields($definition) as $field) {
86
                if ($command->hasField($field->getStorageName())) {
87
                    $command->requestChangeSet();
88
                }
89
            }
90
        }
91
    }
92
93
    public function onEntityWritten(EntityWrittenContainerEvent $event): void
94
    {
95
        $associationFields = $this->getAssociationFields();
96
        $ruleIds = [];
97
98
        foreach ($event->getEvents() ?? [] as $nestedEvent) {
99
            if (!$nestedEvent instanceof EntityWrittenEvent) {
100
                continue;
101
            }
102
103
            $definition = $this->getAssociationDefinitionByEntity($associationFields, $nestedEvent->getEntityName());
104
105
            if (!$definition) {
106
                continue;
107
            }
108
109
            $ruleIds = $this->hydrateRuleIds($this->getForeignKeyFields($definition), $nestedEvent, $ruleIds);
110
        }
111
112
        if (empty($ruleIds)) {
113
            return;
114
        }
115
116
        $this->update(Uuid::fromBytesToHexList(array_unique(array_filter($ruleIds))));
117
118
        $this->cacheInvalidator->invalidate([CachedRuleLoader::CACHE_KEY]);
119
    }
120
121
    /**
122
     * @param list<string> $ids
0 ignored issues
show
Bug introduced by
The type Shopware\Core\Content\Ru...taAbstractionLayer\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
123
     */
124
    public function update(array $ids): void
125
    {
126
        $associationFields = $this->getAssociationFields();
127
128
        $areas = $this->getAreas($ids, $associationFields);
129
130
        $update = new RetryableQuery(
131
            $this->connection,
132
            $this->connection->prepare('UPDATE `rule` SET `areas` = :areas WHERE `id` = :id')
133
        );
134
135
        /** @var array<string, string[]> $associations */
136
        foreach ($areas as $id => $associations) {
137
            $areas = [];
138
139
            foreach ($associations as $propertyName => $match) {
140
                if ((bool) $match === false) {
141
                    continue;
142
                }
143
144
                if ($propertyName === 'flowCondition') {
145
                    $areas = array_unique(array_merge($areas, [RuleAreas::FLOW_CONDITION_AREA]));
146
147
                    continue;
148
                }
149
150
                $field = $associationFields->get($propertyName);
151
152
                if (!$field || !$flag = $field->getFlag(RuleAreas::class)) {
153
                    continue;
154
                }
155
156
                $areas = array_unique(array_merge($areas, $flag instanceof RuleAreas ? $flag->getAreas() : []));
157
            }
158
159
            $update->execute([
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\Framework\...tryableQuery::execute() has been deprecated: tag:v6.5.0 - reason:return-type-change - will return the number of affected rows as int in the next major and $params won't allow null anymore and will be an empty array as default value ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

159
            /** @scrutinizer ignore-deprecated */ $update->execute([

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
160
                'areas' => json_encode(array_values($areas)),
161
                'id' => Uuid::fromHexToBytes($id),
162
            ]);
163
        }
164
    }
165
166
    /**
167
     * @param FkField[] $fields
168
     * @param string[] $ruleIds
169
     *
170
     * @return string[]
171
     */
172
    private function hydrateRuleIds(array $fields, EntityWrittenEvent $nestedEvent, array $ruleIds): array
173
    {
174
        foreach ($nestedEvent->getWriteResults() as $result) {
175
            $changeSet = $result->getChangeSet();
176
            $payload = $result->getPayload();
177
178
            foreach ($fields as $field) {
179
                if ($changeSet && $changeSet->hasChanged($field->getStorageName())) {
180
                    $ruleIds[] = $changeSet->getBefore($field->getStorageName());
181
                    $ruleIds[] = $changeSet->getAfter($field->getStorageName());
182
                }
183
184
                if ($changeSet) {
185
                    continue;
186
                }
187
188
                if (!empty($payload[$field->getPropertyName()])) {
189
                    $ruleIds[] = Uuid::fromHexToBytes($payload[$field->getPropertyName()]);
190
                }
191
            }
192
        }
193
194
        return $ruleIds;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ruleIds returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
195
    }
196
197
    /**
198
     * @param list<string> $ids
199
     *
200
     * @return array<string, string[][]>
201
     */
202
    private function getAreas(array $ids, CompiledFieldCollection $associationFields): array
203
    {
204
        $query = new QueryBuilder($this->connection);
205
        $query->select('LOWER(HEX(`rule`.`id`)) AS array_key')
206
            ->from('rule')
207
            ->andWhere('`rule`.`id` IN (:ids)');
208
209
        /** @var AssociationField $associationField */
210
        foreach ($associationFields->getElements() as $associationField) {
211
            $this->addSelect($query, $associationField);
212
        }
213
        $this->addFlowConditionSelect($query);
214
215
        $query->setParameter(
216
            'ids',
217
            Uuid::fromHexToBytesList($ids),
218
            Connection::PARAM_STR_ARRAY
219
        )->setParameter(
220
            'flowTypes',
221
            $this->conditionRegistry->getFlowRuleNames(),
222
            Connection::PARAM_STR_ARRAY
223
        );
224
225
        return FetchModeHelper::groupUnique($query->executeQuery()->fetchAllAssociative());
226
    }
227
228
    private function addSelect(QueryBuilder $query, AssociationField $associationField): void
229
    {
230
        $template = 'EXISTS(%s) AS %s';
231
        $propertyName = $associationField->getPropertyName();
232
233
        if ($associationField instanceof OneToOneAssociationField || $associationField instanceof ManyToOneAssociationField) {
234
            $template = 'IF(%s.%s IS NOT NULL, 1, 0) AS %s';
235
            $query->addSelect(sprintf($template, '`rule`', $this->escape($associationField->getStorageName()), $propertyName));
236
237
            return;
238
        }
239
240
        if ($associationField instanceof ManyToManyAssociationField) {
241
            $mappingTable = $this->escape($associationField->getMappingDefinition()->getEntityName());
242
            $mappingLocalColumn = $this->escape($associationField->getMappingLocalColumn());
243
            $localColumn = $this->escape($associationField->getLocalField());
244
245
            $subQuery = (new QueryBuilder($this->connection))
246
                ->select('1')
247
                ->from($mappingTable)
248
                ->andWhere(sprintf('%s = `rule`.%s', $mappingLocalColumn, $localColumn));
249
250
            $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
251
252
            return;
253
        }
254
255
        if ($associationField instanceof OneToManyAssociationField) {
256
            $referenceTable = $this->escape($associationField->getReferenceDefinition()->getEntityName());
257
            $referenceColumn = $this->escape($associationField->getReferenceField());
258
            $localColumn = $this->escape($associationField->getLocalField());
259
260
            $subQuery = (new QueryBuilder($this->connection))
261
                ->select('1')
262
                ->from($referenceTable)
263
                ->andWhere(sprintf('%s = `rule`.%s', $referenceColumn, $localColumn));
264
265
            $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
266
        }
267
    }
268
269
    private function addFlowConditionSelect(QueryBuilder $query): void
270
    {
271
        $subQuery = (new QueryBuilder($this->connection))
272
            ->select('1')
273
            ->from('rule_condition')
274
            ->andWhere('`rule_id` = `rule`.`id`')
275
            ->andWhere('`type` IN (:flowTypes)');
276
277
        $query->addSelect(sprintf('EXISTS(%s) AS flowCondition', $subQuery->getSQL()));
278
    }
279
280
    private function escape(string $string): string
281
    {
282
        return EntityDefinitionQueryHelper::escape($string);
283
    }
284
285
    private function getAssociationFields(): CompiledFieldCollection
286
    {
287
        return $this->definition
288
            ->getFields()
289
            ->filterByFlag(RuleAreas::class);
290
    }
291
292
    /**
293
     * @return FkField[]
294
     */
295
    private function getForeignKeyFields(EntityDefinition $definition): array
296
    {
297
        /** @var FkField[] $fields */
298
        $fields = $definition->getFields()->filterInstance(FkField::class)->filter(function (FkField $fk): bool {
299
            return $fk->getReferenceDefinition()->getEntityName() === $this->definition->getEntityName();
300
        })->getElements();
301
302
        return $fields;
303
    }
304
305
    /**
306
     * @return string[]
307
     */
308
    private function getAssociationEntities(): array
309
    {
310
        return $this->getAssociationFields()->filter(function (AssociationField $associationField): bool {
311
            return $associationField instanceof OneToManyAssociationField;
312
        })->map(function (AssociationField $field): string {
313
            return $field->getReferenceDefinition()->getEntityName();
314
        });
315
    }
316
317
    private function getAssociationDefinitionByEntity(CompiledFieldCollection $collection, string $entityName): ?EntityDefinition
318
    {
319
        /** @var AssociationField|null $field */
320
        $field = $collection->filter(function (AssociationField $associationField) use ($entityName): bool {
321
            if (!$associationField instanceof OneToManyAssociationField) {
322
                return false;
323
            }
324
325
            return $associationField->getReferenceDefinition()->getEntityName() === $entityName;
326
        })->first();
327
328
        return $field ? $field->getReferenceDefinition() : null;
329
    }
330
}
331