EntityWriteGateway::getParentField()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 6
nop 1
dl 0
loc 27
rs 9.2222
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
4
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Exception;
7
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilderAlias;
8
use Doctrine\DBAL\Types\Types;
9
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
10
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
11
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
12
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableTransaction;
13
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
14
use Shopware\Core\Framework\DataAbstractionLayer\EntityTranslationDefinition;
15
use Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent;
16
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeleteEvent;
17
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWriteEvent;
18
use Shopware\Core\Framework\DataAbstractionLayer\Exception\CanNotFindParentStorageFieldException;
19
use Shopware\Core\Framework\DataAbstractionLayer\Exception\InvalidParentAssociationException;
20
use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldForeignKeyConstraintMissingException;
21
use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldNotFoundException;
22
use Shopware\Core\Framework\DataAbstractionLayer\Exception\PrimaryKeyNotProvidedException;
23
use Shopware\Core\Framework\DataAbstractionLayer\Exception\UnsupportedCommandTypeException;
24
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
25
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
26
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
27
use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
28
use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
29
use Shopware\Core\Framework\DataAbstractionLayer\MappingEntityDefinition;
30
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSet;
31
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
32
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
33
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
34
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\JsonUpdateCommand;
35
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
36
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
37
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;
38
use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair;
39
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
40
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriteGatewayInterface;
41
use Shopware\Core\Framework\DataAbstractionLayer\Write\PrimaryKeyBag;
42
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;
43
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
44
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\WriteCommandExceptionEvent;
45
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
46
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
47
use Shopware\Core\Framework\Feature;
48
use Shopware\Core\Framework\Log\Package;
49
use Shopware\Core\Framework\Uuid\Uuid;
50
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
51
52
/**
53
 * @internal
54
 */
55
#[Package('core')]
56
class EntityWriteGateway implements EntityWriteGatewayInterface
57
{
58
    private ?PrimaryKeyBag $primaryKeyBag = null;
59
60
    public function __construct(
61
        private readonly int $batchSize,
62
        private readonly Connection $connection,
63
        private readonly EventDispatcherInterface $eventDispatcher,
64
        private readonly ExceptionHandlerRegistry $exceptionHandlerRegistry,
65
        private readonly DefinitionInstanceRegistry $definitionInstanceRegistry
66
    ) {
67
    }
68
69
    public function prefetchExistences(WriteParameterBag $parameters): void
70
    {
71
        $primaryKeyBag = $this->primaryKeyBag = $parameters->getPrimaryKeyBag();
72
73
        if ($primaryKeyBag->isPrefetchingCompleted()) {
74
            return;
75
        }
76
77
        foreach ($primaryKeyBag->getPrimaryKeys() as $entity => $pks) {
78
            $this->prefetch($this->definitionInstanceRegistry->getByEntityName($entity), $pks, $parameters);
79
        }
80
81
        $primaryKeyBag->setPrefetchingCompleted(true);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function getExistence(EntityDefinition $definition, array $primaryKey, array $data, WriteCommandQueue $commandQueue): EntityExistence
88
    {
89
        $state = $this->getCurrentState($definition, $primaryKey, $commandQueue);
90
91
        $exists = !empty($state);
92
93
        $isChild = $this->isChild($definition, $data, $state, $primaryKey, $commandQueue);
94
95
        $wasChild = $this->wasChild($definition, $state);
96
97
        $decodedPrimaryKey = [];
98
        foreach ($primaryKey as $fieldStorageName => $fieldValue) {
99
            $field = $definition->getFields()->getByStorageName($fieldStorageName);
100
            $decodedPrimaryKey[$fieldStorageName] = $field ? $field->getSerializer()->decode($field, $fieldValue) : $fieldValue;
101
        }
102
103
        return new EntityExistence($definition->getEntityName(), $decodedPrimaryKey, $exists, $isChild, $wasChild, $state);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function execute(array $commands, WriteContext $context): void
110
    {
111
        $beforeWriteEvent = EntityWriteEvent::create($context, $commands);
112
113
        $this->eventDispatcher->dispatch($beforeWriteEvent);
114
115
        try {
116
            RetryableTransaction::retryable($this->connection, function () use ($commands, $context): void {
117
                $this->executeCommands($commands, $context);
118
            });
119
120
            $beforeWriteEvent->success();
121
        } catch (\Throwable $e) {
122
            $event = new WriteCommandExceptionEvent($e, $commands, $context->getContext());
123
            $this->eventDispatcher->dispatch($event);
124
125
            $beforeWriteEvent->error();
126
127
            throw $e;
128
        }
129
    }
130
131
    /**
132
     * @param list<WriteCommand> $commands
133
     */
134
    private function executeCommands(array $commands, WriteContext $context): void
135
    {
136
        $entityDeleteEvent = EntityDeleteEvent::create($context, $commands);
137
        $entityDeleteEventLegacy = BeforeDeleteEvent::create($context, $commands);
138
        if ($entityDeleteEvent->filled()) {
139
            $this->eventDispatcher->dispatch($entityDeleteEvent);
140
141
            Feature::ifNotActive('v6.6.0.0', fn () => $this->eventDispatcher->dispatch($entityDeleteEventLegacy));
142
        }
143
144
        // throws exception on violation and then aborts/rollbacks this transaction
145
        $event = new PreWriteValidationEvent($context, $commands);
146
        $this->eventDispatcher->dispatch($event);
147
        /** @var list<WriteCommand> $commands */
148
        $commands = $event->getCommands();
149
150
        $this->generateChangeSets($commands);
151
152
        $context->getExceptions()->tryToThrow();
153
154
        $previous = null;
155
        $mappings = new MultiInsertQueryQueue($this->connection, $this->batchSize, false, true);
156
        $inserts = new MultiInsertQueryQueue($this->connection, $this->batchSize);
157
158
        $executeInserts = function () use ($mappings, $inserts): void {
159
            $mappings->execute();
160
            $inserts->execute();
161
        };
162
163
        try {
164
            foreach ($commands as $command) {
165
                if (!$command->isValid()) {
166
                    continue;
167
                }
168
                $command->setFailed(false);
169
                $current = $command->getDefinition()->getEntityName();
170
171
                if ($current !== $previous) {
172
                    $executeInserts();
173
                }
174
                $previous = $current;
175
176
                try {
177
                    $definition = $command->getDefinition();
178
                    $table = $definition->getEntityName();
179
180
                    if ($command instanceof DeleteCommand) {
181
                        $executeInserts();
182
183
                        RetryableQuery::retryable($this->connection, function () use ($command, $table): void {
184
                            $this->connection->delete(EntityDefinitionQueryHelper::escape($table), $command->getPrimaryKey());
185
                        });
186
187
                        continue;
188
                    }
189
190
                    if ($command instanceof JsonUpdateCommand) {
191
                        $executeInserts();
192
                        $this->executeJsonUpdate($command);
193
194
                        continue;
195
                    }
196
197
                    if ($definition instanceof MappingEntityDefinition && $command instanceof InsertCommand) {
198
                        $mappings->addInsert($definition->getEntityName(), $command->getPayload());
199
200
                        continue;
201
                    }
202
203
                    if ($command instanceof UpdateCommand) {
204
                        $executeInserts();
205
206
                        RetryableQuery::retryable($this->connection, function () use ($command, $table): void {
207
                            $this->connection->update(
208
                                EntityDefinitionQueryHelper::escape($table),
209
                                $this->escapeColumnKeys($command->getPayload()),
210
                                $command->getPrimaryKey()
211
                            );
212
                        });
213
214
                        continue;
215
                    }
216
217
                    if ($command instanceof InsertCommand) {
218
                        $inserts->addInsert($definition->getEntityName(), $command->getPayload());
219
220
                        continue;
221
                    }
222
223
                    throw new UnsupportedCommandTypeException($command);
224
                } catch (\Exception $e) {
225
                    $command->setFailed(true);
226
227
                    $innerException = $this->exceptionHandlerRegistry->matchException($e);
228
229
                    if ($innerException instanceof \Exception) {
230
                        $e = $innerException;
231
                    }
232
                    $context->getExceptions()->add($e);
233
234
                    throw $e;
235
                }
236
            }
237
238
            $mappings->execute();
239
            $inserts->execute();
240
            $entityDeleteEvent->success();
241
            Feature::ifNotActive('v6.6.0.0', fn () => $entityDeleteEventLegacy->success());
242
        } catch (Exception $e) {
243
            // Match exception without passing a specific command when feature-flag 16640 is active
244
            $innerException = $this->exceptionHandlerRegistry->matchException($e);
245
            if ($innerException instanceof \Exception) {
246
                $e = $innerException;
247
            }
248
            $context->getExceptions()->add($e);
249
250
            $entityDeleteEvent->error();
251
            Feature::ifNotActive('v6.6.0.0', fn () => $entityDeleteEventLegacy->error());
252
253
            throw $e;
254
        }
255
256
        // throws exception on violation and then aborts/rollbacks this transaction
257
        $event = new PostWriteValidationEvent($context, $commands);
258
        $this->eventDispatcher->dispatch($event);
259
        $context->getExceptions()->tryToThrow();
260
    }
261
262
    /**
263
     * @param list<array<string, string>> $pks
264
     */
265
    private function prefetch(EntityDefinition $definition, array $pks, WriteParameterBag $parameters): void
266
    {
267
        $pkFields = [];
268
        $versionField = null;
269
        foreach ($definition->getPrimaryKeys() as $field) {
270
            /** @var StorageAware&Field $field */
271
            if ($field instanceof VersionField) {
272
                $versionField = $field;
273
274
                continue;
275
            }
276
            if ($field instanceof StorageAware) {
277
                $pkFields[$field->getStorageName()] = $field;
278
            }
279
        }
280
281
        $query = $this->connection->createQueryBuilder();
282
        $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
283
        $query->addSelect('1 as `exists`');
284
285
        if ($definition->isChildrenAware()) {
286
            $query->addSelect('parent_id');
287
        } elseif ($definition->isInheritanceAware()) {
288
            $parent = $this->getParentField($definition);
289
290
            if ($parent !== null) {
291
                $query->addSelect(
292
                    EntityDefinitionQueryHelper::escape($parent->getStorageName())
293
                    . ' as `parent`'
294
                );
295
            }
296
        }
297
298
        foreach ($pkFields as $storageName => $_) {
299
            $query->addSelect(EntityDefinitionQueryHelper::escape($storageName));
300
        }
301
        if ($versionField) {
302
            $query->addSelect(EntityDefinitionQueryHelper::escape($versionField->getStorageName()));
303
        }
304
305
        $chunks = array_chunk($pks, 500, true);
306
307
        foreach ($chunks as $pks) {
308
            $query->resetQueryPart('where');
309
310
            $params = [];
311
            $tupleCount = 0;
312
313
            foreach ($pks as $pk) {
314
                $newIds = [];
315
                foreach ($pkFields as $field) {
316
                    $id = $pk[$field->getPropertyName()] ?? null;
317
                    if ($id === null) {
318
                        continue 2;
319
                    }
320
                    $newIds[] = $field->getSerializer()->encode(
321
                        $field,
322
                        EntityExistence::createForEntity($definition->getEntityName(), [$field->getPropertyName() => $id]),
323
                        new KeyValuePair($field->getPropertyName(), $id, true),
324
                        $parameters,
325
                    )->current();
326
                }
327
328
                foreach ($newIds as $newId) {
329
                    $params[] = $newId;
330
                }
331
332
                ++$tupleCount;
333
            }
334
335
            if ($tupleCount <= 0) {
336
                continue;
337
            }
338
339
            $placeholders = $this->getPlaceholders(\count($pkFields), $tupleCount);
340
            $columns = '`' . implode('`,`', array_keys($pkFields)) . '`';
341
            if (\count($pkFields) > 1) {
342
                $columns = '(' . $columns . ')';
343
            }
344
345
            $query->andWhere($columns . ' IN (' . $placeholders . ')');
346
            if ($versionField) {
347
                $query->andWhere('version_id = ?');
348
                $params[] = Uuid::fromHexToBytes($parameters->getContext()->getContext()->getVersionId());
349
            }
350
351
            $query->setParameters($params);
352
353
            $result = $query->executeQuery()->fetchAllAssociative();
354
355
            $primaryKeyBag = $parameters->getPrimaryKeyBag();
356
357
            foreach ($result as $state) {
358
                $values = [];
359
                foreach ($pkFields as $storageKey => $field) {
360
                    $values[$field->getPropertyName()] = $field->getSerializer()->decode($field, $state[$storageKey]);
361
                }
362
                if ($versionField) {
363
                    $values[$versionField->getPropertyName()] = $parameters->getContext()->getContext()->getVersionId();
364
                }
365
366
                $primaryKeyBag->addExistenceState($definition, $values, $state);
367
            }
368
369
            foreach ($pks as $pk) {
370
                if (!$primaryKeyBag->hasExistence($definition, $pk)) {
371
                    $primaryKeyBag->addExistenceState($definition, $pk, []);
372
                }
373
            }
374
        }
375
    }
376
377
    /**
378
     * @param array<mixed> $array
379
     */
380
    private static function isAssociative(array $array): bool
381
    {
382
        foreach ($array as $key => $_value) {
383
            if (!\is_int($key)) {
384
                return true;
385
            }
386
        }
387
388
        return false;
389
    }
390
391
    private function executeJsonUpdate(JsonUpdateCommand $command): void
392
    {
393
        /*
394
         * mysql json functions are tricky.
395
         *
396
         * TL;DR: cast objects and arrays to json
397
         *
398
         * This works:
399
         *
400
         * SELECT JSON_SET('{"a": "b"}', '$.a', 7)
401
         * SELECT JSON_SET('{"a": "b"}', '$.a', "str")
402
         *
403
         * This does NOT work:
404
         *
405
         * SELECT JSON_SET('{"a": "b"}', '$.a', '{"foo": "bar"}')
406
         *
407
         * Instead, you have to do this, because mysql cannot differentiate between a string and a json string:
408
         *
409
         * SELECT JSON_SET('{"a": "b"}', '$.a', CAST('{"foo": "bar"}' AS json))
410
         * SELECT JSON_SET('{"a": "b"}', '$.a', CAST('["foo", "bar"]' AS json))
411
         *
412
         * Yet this does NOT work:
413
         *
414
         * SELECT JSON_SET('{"a": "b"}', '$.a', CAST("str" AS json))
415
         *
416
         */
417
418
        $values = [];
419
        $sets = [];
420
        $types = [];
421
422
        $query = new QueryBuilder($this->connection);
423
        $query->update('`' . $command->getDefinition()->getEntityName() . '`');
424
425
        foreach ($command->getPayload() as $attribute => $value) {
426
            // add path and value for each attribute value pair
427
            $values[] = '$."' . $attribute . '"';
428
            $types[] = Types::STRING;
429
            if (\is_array($value) || \is_object($value)) {
430
                $types[] = Types::STRING;
431
                $values[] = json_encode($value, \JSON_PRESERVE_ZERO_FRACTION | \JSON_UNESCAPED_UNICODE);
432
                // does the same thing as CAST(?, json) but works on mariadb
433
                $identityValue = \is_object($value) || self::isAssociative($value) ? '{}' : '[]';
434
                $sets[] = '?, JSON_MERGE("' . $identityValue . '", ?)';
435
            } else {
436
                if (!\is_bool($value)) {
437
                    $values[] = $value;
438
                }
439
440
                $set = '?, ?';
441
442
                if (\is_float($value)) {
443
                    $types[] = \PDO::PARAM_STR;
444
                    $set = '?, ? + 0.0';
445
                } elseif (\is_int($value)) {
446
                    $types[] = \PDO::PARAM_INT;
447
                } elseif (\is_bool($value)) {
448
                    $set = '?, ' . ($value ? 'true' : 'false');
449
                } else {
450
                    $types[] = \PDO::PARAM_STR;
451
                }
452
453
                $sets[] = $set;
454
            }
455
        }
456
457
        $storageName = $command->getStorageName();
458
        $query->set(
459
            $storageName,
460
            sprintf(
461
                'JSON_SET(IFNULL(%s, "{}"), %s)',
462
                EntityDefinitionQueryHelper::escape($storageName),
463
                implode(', ', $sets)
464
            )
465
        );
466
467
        $identifier = $command->getPrimaryKey();
468
        foreach ($identifier as $key => $_value) {
469
            $query->andWhere(EntityDefinitionQueryHelper::escape($key) . ' = ?');
470
        }
471
        $query->setParameters([...$values, ...array_values($identifier)], $types);
472
473
        RetryableQuery::retryable($this->connection, function () use ($query): void {
474
            $query->executeStatement();
475
        });
476
    }
477
478
    /**
479
     * @param array<string, mixed> $payload
480
     *
481
     * @return array<string, mixed>
482
     */
483
    private function escapeColumnKeys(array $payload): array
484
    {
485
        $escaped = [];
486
        foreach ($payload as $key => $value) {
487
            $escaped[EntityDefinitionQueryHelper::escape($key)] = $value;
488
        }
489
490
        return $escaped;
491
    }
492
493
    /**
494
     * @param list<WriteCommand> $commands
495
     */
496
    private function generateChangeSets(array $commands): void
497
    {
498
        $primaryKeys = [];
499
        $definitions = [];
500
501
        foreach ($commands as $command) {
502
            if (!$command instanceof ChangeSetAware || !$command instanceof WriteCommand) {
503
                continue;
504
            }
505
506
            if (!$command->requiresChangeSet()) {
507
                continue;
508
            }
509
510
            $entity = $command->getDefinition()->getEntityName();
511
512
            $primaryKeys[$entity][] = $command->getPrimaryKey();
513
            $definitions[$entity] = $command->getDefinition();
514
        }
515
516
        if (empty($primaryKeys)) {
517
            return;
518
        }
519
520
        $states = [];
521
        foreach ($primaryKeys as $entity => $ids) {
522
            $query = $this->connection->createQueryBuilder();
523
524
            $definition = $definitions[$entity];
525
526
            $query->addSelect('*');
527
            $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
528
529
            $this->addPrimaryCondition($query, $ids);
530
531
            $states[$entity] = $query->executeQuery()->fetchAllAssociative();
532
        }
533
534
        foreach ($commands as $command) {
535
            if (!$command instanceof ChangeSetAware || !$command instanceof WriteCommand) {
536
                continue;
537
            }
538
539
            if (!$command->requiresChangeSet()) {
540
                continue;
541
            }
542
543
            $entity = $command->getDefinition()->getEntityName();
544
545
            $command->setChangeSet(
546
                $this->calculateChangeSet($command, $states[$entity])
547
            );
548
        }
549
    }
550
551
    /**
552
     * @param list<array<string, string>> $primaryKeys
553
     */
554
    private function addPrimaryCondition(DbalQueryBuilderAlias $query, array $primaryKeys): void
555
    {
556
        $all = [];
557
        $i = 0;
558
        foreach ($primaryKeys as $primaryKey) {
559
            $where = [];
560
561
            foreach ($primaryKey as $field => $value) {
562
                ++$i;
563
                $field = EntityDefinitionQueryHelper::escape($field);
564
                $where[] = $field . ' = :param' . $i;
565
                $query->setParameter('param' . $i, $value);
566
            }
567
568
            $all[] = implode(' AND ', $where);
569
        }
570
571
        $query->andWhere(implode(' OR ', $all));
572
    }
573
574
    /**
575
     * @param list<array<string, mixed>> $states
576
     */
577
    private function calculateChangeSet(WriteCommand $command, array $states): ChangeSet
578
    {
579
        foreach ($states as $state) {
580
            // check if current loop matches the command primary key
581
            $primaryKey = array_intersect($command->getPrimaryKey(), $state);
582
583
            if (\count(array_diff_assoc($command->getPrimaryKey(), $primaryKey)) === 0) {
584
                return new ChangeSet($state, $command->getPayload(), $command instanceof DeleteCommand);
585
            }
586
        }
587
588
        return new ChangeSet([], [], $command instanceof DeleteCommand);
589
    }
590
591
    private function getPlaceholders(int $columnCount, int $tupleCount): string
592
    {
593
        if ($columnCount > 1) {
594
            // multi column pk. Example: (product_id, language_id) IN ((p1, l1), (p2, l2), (px,lx),...)
595
            $tupleStr = '(?' . str_repeat(',?', $columnCount - 1) . ')';
596
        } else {
597
            // single column pk. Example: category_id IN (c1, c2, c3,...)
598
            $tupleStr = '?';
599
        }
600
601
        return $tupleStr . str_repeat(',' . $tupleStr, $tupleCount - 1);
602
    }
603
604
    private function getParentField(EntityDefinition $definition): ?FkField
605
    {
606
        if (!$definition->isInheritanceAware()) {
607
            return null;
608
        }
609
610
        /** @var ManyToOneAssociationField|null $parent */
611
        $parent = $definition->getFields()->get('parent');
612
613
        if (!$parent) {
614
            throw new ParentFieldNotFoundException($definition);
615
        }
616
617
        if (!$parent instanceof ManyToOneAssociationField) {
618
            throw new InvalidParentAssociationException($definition, $parent);
619
        }
620
621
        $fk = $definition->getFields()->getByStorageName($parent->getStorageName());
622
623
        if (!$fk) {
624
            throw new CanNotFindParentStorageFieldException($definition);
625
        }
626
        if (!$fk instanceof FkField) {
627
            throw new ParentFieldForeignKeyConstraintMissingException($definition, $fk);
628
        }
629
630
        return $fk;
631
    }
632
633
    /**
634
     * @param array<string, string> $primaryKey
635
     *
636
     * @return array<string, mixed>
637
     */
638
    private function getCurrentState(EntityDefinition $definition, array $primaryKey, WriteCommandQueue $commandQueue): array
639
    {
640
        $commands = $commandQueue->getCommandsForEntity($definition, $primaryKey);
641
642
        $useDatabase = true;
643
644
        $state = [];
645
646
        foreach ($commands as $command) {
647
            if ($command instanceof DeleteCommand) {
648
                $state = [];
649
                $useDatabase = false;
650
651
                continue;
652
            }
653
654
            if (!$command instanceof InsertCommand && !$command instanceof UpdateCommand) {
655
                continue;
656
            }
657
658
            $state = array_replace_recursive($state, $command->getPayload());
659
660
            if ($command instanceof InsertCommand) {
661
                $useDatabase = false;
662
            }
663
        }
664
665
        if (!$useDatabase) {
666
            return $state;
667
        }
668
669
        $decodedPrimaryKey = [];
670
        foreach ($primaryKey as $fieldName => $fieldValue) {
671
            $field = $definition->getField($fieldName);
672
            $decodedPrimaryKey[$fieldName] = $field ? $field->getSerializer()->decode($field, $fieldValue) : $fieldValue;
673
        }
674
675
        $currentState = $this->primaryKeyBag === null ? null : $this->primaryKeyBag->getExistenceState($definition, $decodedPrimaryKey);
676
        if ($currentState === null) {
677
            $currentState = $this->fetchFromDatabase($definition, $primaryKey);
678
        }
679
680
        $parent = $this->getParentField($definition);
681
682
        if ($parent && \array_key_exists('parent', $currentState)) {
683
            $currentState[$parent->getStorageName()] = $currentState['parent'];
684
            unset($currentState['parent']);
685
        }
686
687
        return array_replace_recursive($currentState, $state);
688
    }
689
690
    /**
691
     * @param array<string, string> $primaryKey
692
     *
693
     * @return array<string, mixed>
694
     */
695
    private function fetchFromDatabase(EntityDefinition $definition, array $primaryKey): array
696
    {
697
        $query = $this->connection->createQueryBuilder();
698
        $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
699
700
        $fields = $definition->getPrimaryKeys();
701
702
        /** @var Field&StorageAware $field */
703
        foreach ($fields as $field) {
704
            if (!\array_key_exists($field->getStorageName(), $primaryKey)) {
705
                if (!\array_key_exists($field->getPropertyName(), $primaryKey)) {
706
                    throw new PrimaryKeyNotProvidedException($definition, $field);
707
                }
708
709
                $primaryKey[$field->getStorageName()] = $primaryKey[$field->getPropertyName()];
710
                unset($primaryKey[$field->getPropertyName()]);
711
            }
712
713
            $param = 'param_' . Uuid::randomHex();
714
            $query->andWhere(EntityDefinitionQueryHelper::escape($field->getStorageName()) . ' = :' . $param);
715
            $query->setParameter($param, $primaryKey[$field->getStorageName()]);
716
        }
717
718
        $query->addSelect('1 as `exists`');
719
720
        if ($definition->isChildrenAware()) {
721
            $query->addSelect('parent_id');
722
        } elseif ($definition->isInheritanceAware()) {
723
            $parent = $this->getParentField($definition);
724
725
            if ($parent !== null) {
726
                $query->addSelect(
727
                    EntityDefinitionQueryHelper::escape($parent->getStorageName())
728
                    . ' as `parent`'
729
                );
730
            }
731
        }
732
733
        $exists = $query->executeQuery()->fetchAssociative();
734
        if (!$exists) {
735
            $exists = [];
736
        }
737
738
        return $exists;
739
    }
740
741
    /**
742
     * @param array<string, mixed> $data
743
     * @param array<string, mixed> $state
744
     * @param array<string, string> $primaryKey
745
     */
746
    private function isChild(EntityDefinition $definition, array $data, array $state, array $primaryKey, WriteCommandQueue $commandQueue): bool
747
    {
748
        if ($definition instanceof EntityTranslationDefinition) {
749
            return $this->isTranslationChild($definition, $primaryKey, $commandQueue);
750
        }
751
752
        if (!$definition->isInheritanceAware()) {
753
            return false;
754
        }
755
756
        $fk = $this->getParentField($definition);
757
758
        \assert($fk instanceof FkField);
759
760
        // foreign key provided, !== null has parent otherwise not
761
        if (\array_key_exists($fk->getPropertyName(), $data)) {
762
            return isset($data[$fk->getPropertyName()]);
763
        }
764
765
        /** @var Field $association */
766
        $association = $definition->getFields()->get('parent');
767
        if (isset($data[$association->getPropertyName()])) {
768
            return true;
769
        }
770
771
        return isset($state[$fk->getStorageName()]);
772
    }
773
774
    /**
775
     * @param array<string, mixed> $state
776
     */
777
    private function wasChild(EntityDefinition $definition, array $state): bool
778
    {
779
        if (!$definition->isInheritanceAware()) {
780
            return false;
781
        }
782
783
        $fk = $this->getParentField($definition);
784
785
        return $fk !== null && isset($state[$fk->getStorageName()]);
786
    }
787
788
    /**
789
     * @param array<string, string> $primaryKey
790
     */
791
    private function isTranslationChild(EntityTranslationDefinition $definition, array $primaryKey, WriteCommandQueue $commandQueue): bool
792
    {
793
        $parent = $definition->getParentDefinition();
794
795
        if (!$parent->isInheritanceAware()) {
796
            return false;
797
        }
798
799
        /** @var FkField $fkField */
800
        $fkField = $definition->getFields()->getByStorageName(
801
            $parent->getEntityName() . '_id'
802
        );
803
        $parentPrimaryKey = [
804
            'id' => $primaryKey[$fkField->getStorageName()],
805
        ];
806
807
        if ($parent->isVersionAware()) {
808
            $parentPrimaryKey['versionId'] = $primaryKey[$parent->getEntityName() . '_version_id'];
809
        }
810
811
        $existence = $this->getExistence($parent, $parentPrimaryKey, [], $commandQueue);
812
813
        return $existence->isChild();
814
    }
815
}
816