Passed
Push — trunk ( 95cc21...bc3fdd )
by Christian
12:22 queued 15s
created

RuleAreaUpdater::triggerChangeSet()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 12
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 21
rs 8.8333
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Content\Rule\DataAbstractionLayer;
4
5
use Doctrine\DBAL\Connection;
6
use Shopware\Core\Content\Rule\RuleDefinition;
7
use Shopware\Core\Framework\DataAbstractionLayer\CompiledFieldCollection;
8
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
9
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder;
10
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
11
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
12
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
13
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
14
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
15
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
16
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
17
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas;
18
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
19
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
20
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
21
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
22
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
23
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
24
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
25
use Shopware\Core\Framework\Uuid\Uuid;
26
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
27
28
/**
29
 * @internal
30
 */
31
class RuleAreaUpdater implements EventSubscriberInterface
32
{
33
    private Connection $connection;
34
35
    private RuleDefinition $definition;
36
37
    /**
38
     * @internal
39
     */
40
    public function __construct(Connection $connection, RuleDefinition $definition)
41
    {
42
        $this->connection = $connection;
43
        $this->definition = $definition;
44
    }
45
46
    public static function getSubscribedEvents(): array
47
    {
48
        return [
49
            PreWriteValidationEvent::class => 'triggerChangeSet',
50
            EntityWrittenContainerEvent::class => 'onEntityWritten',
51
        ];
52
    }
53
54
    public function triggerChangeSet(PreWriteValidationEvent $event): void
55
    {
56
        $associatedEntities = $this->getAssociationEntities();
57
58
        foreach ($event->getCommands() as $command) {
59
            $definition = $command->getDefinition();
60
            $entity = $definition->getEntityName();
61
62
            if (!$command instanceof ChangeSetAware || !\in_array($entity, $associatedEntities, true)) {
63
                continue;
64
            }
65
66
            if ($command instanceof DeleteCommand) {
67
                $command->requestChangeSet();
68
69
                continue;
70
            }
71
72
            foreach ($this->getForeignKeyFields($definition) as $field) {
73
                if ($command->hasField($field->getStorageName())) {
74
                    $command->requestChangeSet();
75
                }
76
            }
77
        }
78
    }
79
80
    public function onEntityWritten(EntityWrittenContainerEvent $event): void
81
    {
82
        $associationFields = $this->getAssociationFields();
83
        $ruleIds = [];
84
85
        foreach ($event->getEvents() ?? [] as $nestedEvent) {
86
            if (!$nestedEvent instanceof EntityWrittenEvent) {
87
                continue;
88
            }
89
90
            $definition = $this->getAssociationDefinitionByEntity($associationFields, $nestedEvent->getEntityName());
91
92
            if (!$definition) {
93
                continue;
94
            }
95
96
            $ruleIds = $this->hydrateRuleIds($this->getForeignKeyFields($definition), $nestedEvent, $ruleIds);
97
        }
98
99
        if (empty($ruleIds)) {
100
            return;
101
        }
102
103
        $this->update(Uuid::fromBytesToHexList(array_unique(array_filter($ruleIds))));
104
    }
105
106
    /**
107
     * @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...
108
     */
109
    public function update(array $ids): void
110
    {
111
        $associationFields = $this->getAssociationFields();
112
113
        $areas = $this->getAreas($ids, $associationFields);
114
115
        $update = new RetryableQuery(
116
            $this->connection,
117
            $this->connection->prepare('UPDATE `rule` SET `areas` = :areas WHERE `id` = :id')
118
        );
119
120
        /** @var array<string, string[]> $associations */
121
        foreach ($areas as $id => $associations) {
122
            $areas = [];
123
124
            foreach ($associations as $propertyName => $match) {
125
                if ((bool) $match === false) {
126
                    continue;
127
                }
128
129
                $field = $associationFields->get($propertyName);
130
131
                if (!$field || !$flag = $field->getFlag(RuleAreas::class)) {
132
                    continue;
133
                }
134
135
                $areas = array_unique(array_merge($areas, $flag instanceof RuleAreas ? $flag->getAreas() : []));
136
            }
137
138
            $update->execute([
139
                'areas' => json_encode($areas),
140
                'id' => Uuid::fromHexToBytes($id),
141
            ]);
142
        }
143
    }
144
145
    /**
146
     * @param FkField[] $fields
147
     * @param string[] $ruleIds
148
     *
149
     * @return string[]
150
     */
151
    private function hydrateRuleIds(array $fields, EntityWrittenEvent $nestedEvent, array $ruleIds): array
152
    {
153
        foreach ($nestedEvent->getWriteResults() as $result) {
154
            $changeSet = $result->getChangeSet();
155
            $payload = $result->getPayload();
156
157
            foreach ($fields as $field) {
158
                if ($changeSet && $changeSet->hasChanged($field->getStorageName())) {
159
                    $ruleIds[] = $changeSet->getBefore($field->getStorageName());
160
                    $ruleIds[] = $changeSet->getAfter($field->getStorageName());
161
                }
162
163
                if ($changeSet) {
164
                    continue;
165
                }
166
167
                if (!empty($payload[$field->getPropertyName()])) {
168
                    $ruleIds[] = Uuid::fromHexToBytes($payload[$field->getPropertyName()]);
169
                }
170
            }
171
        }
172
173
        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...
174
    }
175
176
    /**
177
     * @param list<string> $ids
178
     *
179
     * @return array<string, string[][]>
180
     */
181
    private function getAreas(array $ids, CompiledFieldCollection $associationFields): array
182
    {
183
        $query = new QueryBuilder($this->connection);
184
        $query->select('LOWER(HEX(`rule`.`id`)) AS array_key')
185
            ->from('rule')
186
            ->andWhere('`rule`.`id` IN (:ids)');
187
188
        /** @var AssociationField $associationField */
189
        foreach ($associationFields->getElements() as $associationField) {
190
            $this->addSelect($query, $associationField);
191
        }
192
193
        $query->setParameter(
194
            'ids',
195
            Uuid::fromHexToBytesList($ids),
196
            Connection::PARAM_STR_ARRAY
197
        );
198
199
        return FetchModeHelper::groupUnique($query->execute()->fetchAllAssociative());
200
    }
201
202
    private function addSelect(QueryBuilder $query, AssociationField $associationField): void
203
    {
204
        $template = 'EXISTS(%s) AS %s';
205
        $propertyName = $associationField->getPropertyName();
206
207
        if ($associationField instanceof OneToOneAssociationField || $associationField instanceof ManyToOneAssociationField) {
208
            $template = 'IF(%s.%s IS NOT NULL, 1, 0) AS %s';
209
            $query->addSelect(sprintf($template, '`rule`', $this->escape($associationField->getStorageName()), $propertyName));
210
211
            return;
212
        }
213
214
        if ($associationField instanceof ManyToManyAssociationField) {
215
            $mappingTable = $this->escape($associationField->getMappingDefinition()->getEntityName());
216
            $mappingLocalColumn = $this->escape($associationField->getMappingLocalColumn());
217
            $localColumn = $this->escape($associationField->getLocalField());
218
219
            $subQuery = (new QueryBuilder($this->connection))
220
                ->select('1')
221
                ->from($mappingTable)
222
                ->andWhere(sprintf('%s = `rule`.%s', $mappingLocalColumn, $localColumn));
223
224
            $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
225
226
            return;
227
        }
228
229
        if ($associationField instanceof OneToManyAssociationField) {
230
            $referenceTable = $this->escape($associationField->getReferenceDefinition()->getEntityName());
231
            $referenceColumn = $this->escape($associationField->getReferenceField());
232
            $localColumn = $this->escape($associationField->getLocalField());
233
234
            $subQuery = (new QueryBuilder($this->connection))
235
                ->select('1')
236
                ->from($referenceTable)
237
                ->andWhere(sprintf('%s = `rule`.%s', $referenceColumn, $localColumn));
238
239
            $query->addSelect(sprintf($template, $subQuery->getSQL(), $propertyName));
240
        }
241
    }
242
243
    private function escape(string $string): string
244
    {
245
        return EntityDefinitionQueryHelper::escape($string);
246
    }
247
248
    private function getAssociationFields(): CompiledFieldCollection
249
    {
250
        return $this->definition
251
            ->getFields()
252
            ->filterByFlag(RuleAreas::class);
253
    }
254
255
    /**
256
     * @return FkField[]
257
     */
258
    private function getForeignKeyFields(EntityDefinition $definition): array
259
    {
260
        /** @var FkField[] $fields */
261
        $fields = $definition->getFields()->filterInstance(FkField::class)->filter(function (FkField $fk): bool {
262
            return $fk->getReferenceDefinition()->getEntityName() === $this->definition->getEntityName();
263
        })->getElements();
264
265
        return $fields;
266
    }
267
268
    /**
269
     * @return string[]
270
     */
271
    private function getAssociationEntities(): array
272
    {
273
        return $this->getAssociationFields()->filter(function (AssociationField $associationField): bool {
274
            return $associationField instanceof OneToManyAssociationField;
275
        })->map(function (AssociationField $field): string {
276
            return $field->getReferenceDefinition()->getEntityName();
277
        });
278
    }
279
280
    private function getAssociationDefinitionByEntity(CompiledFieldCollection $collection, string $entityName): ?EntityDefinition
281
    {
282
        /** @var AssociationField|null $field */
283
        $field = $collection->filter(function (AssociationField $associationField) use ($entityName): bool {
284
            if (!$associationField instanceof OneToManyAssociationField) {
285
                return false;
286
            }
287
288
            return $associationField->getReferenceDefinition()->getEntityName() === $entityName;
289
        })->first();
290
291
        return $field ? $field->getReferenceDefinition() : null;
292
    }
293
}
294