| Total Complexity | 123 |
| Total Lines | 808 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like VersionManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use VersionManager, and based on these observations, apply Extract Interface, too.
| 1 | <?php declare(strict_types=1); |
||
| 56 | #[Package('core')] |
||
| 57 | class VersionManager |
||
| 58 | { |
||
| 59 | final public const DISABLE_AUDIT_LOG = 'disable-audit-log'; |
||
| 60 | final public const MERGE_SCOPE = 'merge-scope'; |
||
| 61 | |||
| 62 | public function __construct( |
||
| 75 | } |
||
| 76 | |||
| 77 | /** |
||
| 78 | * @param array<array<string, mixed|null>> $rawData |
||
| 79 | * |
||
| 80 | * @return array<string, array<EntityWriteResult>> |
||
| 81 | */ |
||
| 82 | public function upsert(EntityDefinition $definition, array $rawData, WriteContext $writeContext): array |
||
| 83 | { |
||
| 84 | $result = $this->entityWriter->upsert($definition, $rawData, $writeContext); |
||
| 85 | |||
| 86 | $this->writeAuditLog($result, $writeContext); |
||
| 87 | |||
| 88 | return $result; |
||
| 89 | } |
||
| 90 | |||
| 91 | /** |
||
| 92 | * @param array<array<string, mixed|null>> $rawData |
||
| 93 | * |
||
| 94 | * @return array<string, array<EntityWriteResult>> |
||
| 95 | */ |
||
| 96 | public function insert(EntityDefinition $definition, array $rawData, WriteContext $writeContext): array |
||
| 97 | { |
||
| 98 | /** @var array<string, array<EntityWriteResult>> $result */ |
||
| 99 | $result = $this->entityWriter->insert($definition, $rawData, $writeContext); |
||
| 100 | |||
| 101 | $this->writeAuditLog($result, $writeContext); |
||
| 102 | |||
| 103 | return $result; |
||
| 104 | } |
||
| 105 | |||
| 106 | /** |
||
| 107 | * @param array<array<string, mixed|null>> $rawData |
||
| 108 | * |
||
| 109 | * @return array<string, array<EntityWriteResult>> |
||
| 110 | */ |
||
| 111 | public function update(EntityDefinition $definition, array $rawData, WriteContext $writeContext): array |
||
| 112 | { |
||
| 113 | /** @var array<string, array<EntityWriteResult>> $result */ |
||
| 114 | $result = $this->entityWriter->update($definition, $rawData, $writeContext); |
||
| 115 | |||
| 116 | $this->writeAuditLog($result, $writeContext); |
||
| 117 | |||
| 118 | return $result; |
||
| 119 | } |
||
| 120 | |||
| 121 | /** |
||
| 122 | * @param array<array<string, mixed|null>> $ids |
||
| 123 | */ |
||
| 124 | public function delete(EntityDefinition $definition, array $ids, WriteContext $writeContext): WriteResult |
||
| 125 | { |
||
| 126 | $result = $this->entityWriter->delete($definition, $ids, $writeContext); |
||
| 127 | |||
| 128 | $this->writeAuditLog($result->getDeleted(), $writeContext); |
||
| 129 | |||
| 130 | return $result; |
||
| 131 | } |
||
| 132 | |||
| 133 | public function createVersion(EntityDefinition $definition, string $id, WriteContext $context, ?string $name = null, ?string $versionId = null): string |
||
| 156 | } |
||
| 157 | |||
| 158 | public function merge(string $versionId, WriteContext $writeContext): void |
||
| 159 | { |
||
| 160 | // acquire a lock to prevent multiple merges of the same version |
||
| 161 | $lock = $this->lockFactory->createLock('sw-merge-version-' . $versionId); |
||
| 162 | |||
| 163 | if (!$lock->acquire()) { |
||
| 164 | throw DataAbstractionLayerException::versionMergeAlreadyLocked($versionId); |
||
| 165 | } |
||
| 166 | |||
| 167 | // load all commits of the provided version |
||
| 168 | $commits = $this->getCommits($versionId, $writeContext); |
||
| 169 | |||
| 170 | // create context for live and version |
||
| 171 | $versionContext = $writeContext->createWithVersionId($versionId); |
||
| 172 | $liveContext = $writeContext->createWithVersionId(Defaults::LIVE_VERSION); |
||
| 173 | |||
| 174 | $versionContext->addState(self::MERGE_SCOPE); |
||
| 175 | $liveContext->addState(self::MERGE_SCOPE); |
||
| 176 | |||
| 177 | // group all payloads by their action (insert, update, delete) and by their entity name |
||
| 178 | $writes = $this->buildWrites($commits); |
||
| 179 | |||
| 180 | // execute writes and get access to the write result to dispatch events later on |
||
| 181 | $result = $this->executeWrites($writes, $liveContext); |
||
| 182 | |||
| 183 | // remove commits which reference the version and create a "merge commit" for the live version with all payloads |
||
| 184 | $this->updateVersionData($commits, $writeContext, $versionId); |
||
| 185 | |||
| 186 | // delete all versioned records |
||
| 187 | $this->deleteClones($commits, $versionContext, $versionId); |
||
| 188 | |||
| 189 | // release lock to ensure no other merge is running |
||
| 190 | $lock->release(); |
||
| 191 | |||
| 192 | // dispatch events to trigger indexer and other subscribts |
||
| 193 | $writes = EntityWrittenContainerEvent::createWithWrittenEvents($result->getWritten(), $liveContext->getContext(), []); |
||
| 194 | |||
| 195 | $deletes = EntityWrittenContainerEvent::createWithDeletedEvents($result->getDeleted(), $liveContext->getContext(), []); |
||
| 196 | |||
| 197 | if ($deletes->getEvents() !== null) { |
||
| 198 | $writes->addEvent(...$deletes->getEvents()->getElements()); |
||
| 199 | } |
||
| 200 | $this->eventDispatcher->dispatch($writes); |
||
| 201 | |||
| 202 | $versionContext->removeState(self::MERGE_SCOPE); |
||
| 203 | $liveContext->addState(self::MERGE_SCOPE); |
||
| 204 | } |
||
| 205 | |||
| 206 | /** |
||
| 207 | * @return array<string, array<EntityWriteResult>> |
||
| 208 | */ |
||
| 209 | public function clone( |
||
| 210 | EntityDefinition $definition, |
||
| 211 | string $id, |
||
| 212 | string $newId, |
||
| 213 | string $versionId, |
||
| 214 | WriteContext $context, |
||
| 215 | CloneBehavior $behavior |
||
| 216 | ): array { |
||
| 217 | return $this->cloneEntity($definition, $id, $newId, $versionId, $context, $behavior, true); |
||
| 218 | } |
||
| 219 | |||
| 220 | /** |
||
| 221 | * @return array<string, array<EntityWriteResult>> |
||
| 222 | */ |
||
| 223 | private function cloneEntity( |
||
| 224 | EntityDefinition $definition, |
||
| 225 | string $id, |
||
| 226 | string $newId, |
||
| 227 | string $versionId, |
||
| 228 | WriteContext $context, |
||
| 229 | CloneBehavior $behavior, |
||
| 230 | bool $writeAuditLog = false |
||
| 231 | ): array { |
||
| 232 | $criteria = new Criteria([$id]); |
||
| 233 | $this->addCloneAssociations($definition, $criteria, $behavior->cloneChildren()); |
||
| 234 | |||
| 235 | $detail = $this->entityReader->read($definition, $criteria, $context->getContext())->first(); |
||
| 236 | |||
| 237 | if ($detail === null) { |
||
| 238 | throw DataAbstractionLayerException::cannotCreateNewVersion($definition->getEntityName(), $id); |
||
| 239 | } |
||
| 240 | |||
| 241 | $data = json_decode($this->serializer->serialize($detail, 'json'), true, 512, \JSON_THROW_ON_ERROR); |
||
| 242 | |||
| 243 | $keepIds = $newId === $id; |
||
| 244 | |||
| 245 | $data = $this->filterPropertiesForClone($definition, $data, $keepIds, $id, $definition, $context->getContext()); |
||
| 246 | $data['id'] = $newId; |
||
| 247 | |||
| 248 | $createdAtField = $definition->getField('createdAt'); |
||
| 249 | $updatedAtField = $definition->getField('updatedAt'); |
||
| 250 | |||
| 251 | if ($createdAtField instanceof DateTimeField) { |
||
| 252 | $data['createdAt'] = new \DateTime(); |
||
| 253 | } |
||
| 254 | |||
| 255 | if ($updatedAtField instanceof DateTimeField) { |
||
| 256 | if ($updatedAtField->getFlag(Required::class)) { |
||
| 257 | $data['updatedAt'] = new \DateTime(); |
||
| 258 | } else { |
||
| 259 | $data['updatedAt'] = null; |
||
| 260 | } |
||
| 261 | } |
||
| 262 | |||
| 263 | $data = array_replace_recursive($data, $behavior->getOverwrites()); |
||
| 264 | |||
| 265 | $versionContext = $context->createWithVersionId($versionId); |
||
| 266 | $result = null; |
||
| 267 | $versionContext->scope(Context::SYSTEM_SCOPE, function (WriteContext $context) use ($definition, $data, &$result): void { |
||
| 268 | $result = $this->entityWriter->insert($definition, [$data], $context); |
||
| 269 | }); |
||
| 270 | |||
| 271 | if ($writeAuditLog) { |
||
| 272 | $this->writeAuditLog($result, $versionContext); |
||
| 273 | } |
||
| 274 | |||
| 275 | return $result; |
||
| 276 | } |
||
| 277 | |||
| 278 | /** |
||
| 279 | * @param array<string, array<string, mixed|null>|null> $data |
||
| 280 | * |
||
| 281 | * @return array<string, array<string, mixed|null>|string|null> |
||
| 282 | */ |
||
| 283 | private function filterPropertiesForClone(EntityDefinition $definition, array $data, bool $keepIds, string $cloneId, EntityDefinition $cloneDefinition, Context $context): array |
||
| 284 | { |
||
| 285 | $extensions = []; |
||
| 286 | $payload = []; |
||
| 287 | |||
| 288 | $fields = $definition->getFields(); |
||
| 289 | |||
| 290 | foreach ($fields as $field) { |
||
| 291 | /** @var WriteProtected|null $writeProtection */ |
||
| 292 | $writeProtection = $field->getFlag(WriteProtected::class); |
||
| 293 | if ($writeProtection && !$writeProtection->isAllowed(Context::SYSTEM_SCOPE)) { |
||
| 294 | continue; |
||
| 295 | } |
||
| 296 | |||
| 297 | // set data and payload cursor to root or extensions to simplify following if conditions |
||
| 298 | $dataCursor = $data; |
||
| 299 | |||
| 300 | $payloadCursor = &$payload; |
||
| 301 | |||
| 302 | if ($field instanceof VersionField || $field instanceof ReferenceVersionField) { |
||
| 303 | continue; |
||
| 304 | } |
||
| 305 | |||
| 306 | if ($field->is(Extension::class)) { |
||
| 307 | $dataCursor = $data['extensions'] ?? []; |
||
| 308 | $payloadCursor = &$extensions; |
||
| 309 | if (isset($dataCursor['foreignKeys'])) { |
||
| 310 | $fields = $definition->getFields(); |
||
| 311 | /** |
||
| 312 | * @var string $key |
||
| 313 | * @var string $value |
||
| 314 | */ |
||
| 315 | foreach ($dataCursor['foreignKeys'] as $key => $value) { |
||
| 316 | // Clone FK extension and add it to payload |
||
| 317 | if (\is_string($value) && Uuid::isValid($value) && $fields->has($key) && $fields->get($key) instanceof FkField) { |
||
| 318 | $payload[$key] = $value; |
||
| 319 | } |
||
| 320 | } |
||
| 321 | } |
||
| 322 | } |
||
| 323 | |||
| 324 | if (!\array_key_exists($field->getPropertyName(), $dataCursor)) { |
||
| 325 | continue; |
||
| 326 | } |
||
| 327 | |||
| 328 | if (!$keepIds && $field instanceof ParentFkField) { |
||
| 329 | continue; |
||
| 330 | } |
||
| 331 | |||
| 332 | $value = $dataCursor[$field->getPropertyName()]; |
||
| 333 | |||
| 334 | // remove reference of cloned entity in all sub entity routes. Appears in a parent-child nested data tree |
||
| 335 | if ($field instanceof FkField && !$keepIds && $value === $cloneId && $cloneDefinition === $field->getReferenceDefinition()) { |
||
| 336 | continue; |
||
| 337 | } |
||
| 338 | |||
| 339 | if ($value === null) { |
||
| 340 | continue; |
||
| 341 | } |
||
| 342 | |||
| 343 | // scalar value? assign directly |
||
| 344 | if (!$field instanceof AssociationField) { |
||
| 345 | $payloadCursor[$field->getPropertyName()] = $value; |
||
| 346 | |||
| 347 | continue; |
||
| 348 | } |
||
| 349 | |||
| 350 | // many to one should be skipped because it is no part of the root entity |
||
| 351 | if ($field instanceof ManyToOneAssociationField) { |
||
| 352 | continue; |
||
| 353 | } |
||
| 354 | |||
| 355 | /** @var CascadeDelete|null $flag */ |
||
| 356 | $flag = $field->getFlag(CascadeDelete::class); |
||
| 357 | if (!$flag || !$flag->isCloneRelevant()) { |
||
| 358 | continue; |
||
| 359 | } |
||
| 360 | |||
| 361 | if ($field instanceof OneToManyAssociationField) { |
||
| 362 | $reference = $field->getReferenceDefinition(); |
||
| 363 | |||
| 364 | $nested = []; |
||
| 365 | foreach ($value as $item) { |
||
| 366 | $nestedItem = $this->filterPropertiesForClone($reference, $item, $keepIds, $cloneId, $cloneDefinition, $context); |
||
| 367 | |||
| 368 | if (!$keepIds) { |
||
| 369 | $nestedItem = $this->removePrimaryKey($field, $nestedItem); |
||
| 370 | } |
||
| 371 | |||
| 372 | $nested[] = $nestedItem; |
||
| 373 | } |
||
| 374 | |||
| 375 | $nested = array_filter($nested); |
||
| 376 | if (empty($nested)) { |
||
| 377 | continue; |
||
| 378 | } |
||
| 379 | |||
| 380 | $payloadCursor[$field->getPropertyName()] = $nested; |
||
| 381 | |||
| 382 | continue; |
||
| 383 | } |
||
| 384 | |||
| 385 | if ($field instanceof ManyToManyAssociationField) { |
||
| 386 | $nested = []; |
||
| 387 | |||
| 388 | foreach ($value as $item) { |
||
| 389 | $nested[] = ['id' => $item['id']]; |
||
| 390 | } |
||
| 391 | |||
| 392 | if (empty($nested)) { |
||
| 393 | continue; |
||
| 394 | } |
||
| 395 | |||
| 396 | $payloadCursor[$field->getPropertyName()] = $nested; |
||
| 397 | |||
| 398 | continue; |
||
| 399 | } |
||
| 400 | |||
| 401 | if ($field instanceof OneToOneAssociationField && $value) { |
||
| 402 | $reference = $field->getReferenceDefinition(); |
||
| 403 | |||
| 404 | $nestedItem = $this->filterPropertiesForClone($reference, $value, $keepIds, $cloneId, $cloneDefinition, $context); |
||
| 405 | |||
| 406 | if (!$keepIds) { |
||
| 407 | $nestedItem = $this->removePrimaryKey($field, $nestedItem); |
||
| 408 | } |
||
| 409 | |||
| 410 | $payloadCursor[$field->getPropertyName()] = $nestedItem; |
||
| 411 | } |
||
| 412 | } |
||
| 413 | |||
| 414 | if (!empty($extensions)) { |
||
| 415 | $payload['extensions'] = $extensions; |
||
| 416 | } |
||
| 417 | |||
| 418 | return $payload; |
||
| 419 | } |
||
| 420 | |||
| 421 | /** |
||
| 422 | * @param array<string, array<EntityWriteResult>> $writtenEvents |
||
| 423 | */ |
||
| 424 | private function writeAuditLog(array $writtenEvents, WriteContext $writeContext, ?string $versionId = null, bool $isClone = false): void |
||
| 425 | { |
||
| 426 | if ($writeContext->getContext()->hasState(self::DISABLE_AUDIT_LOG)) { |
||
| 427 | return; |
||
| 428 | } |
||
| 429 | |||
| 430 | $versionId ??= $writeContext->getContext()->getVersionId(); |
||
| 431 | if ($versionId === Defaults::LIVE_VERSION) { |
||
| 432 | return; |
||
| 433 | } |
||
| 434 | |||
| 435 | $commitId = Uuid::randomBytes(); |
||
| 436 | |||
| 437 | $date = (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT); |
||
| 438 | |||
| 439 | $source = $writeContext->getContext()->getSource(); |
||
| 440 | $userId = $source instanceof AdminApiSource && $source->getUserId() |
||
| 441 | ? Uuid::fromHexToBytes($source->getUserId()) |
||
| 442 | : null; |
||
| 443 | |||
| 444 | $insert = new InsertCommand( |
||
| 445 | $this->versionCommitDefinition, |
||
| 446 | [ |
||
| 447 | 'id' => $commitId, |
||
| 448 | 'user_id' => $userId, |
||
| 449 | 'version_id' => Uuid::fromHexToBytes($versionId), |
||
| 450 | 'created_at' => $date, |
||
| 451 | ], |
||
| 452 | ['id' => $commitId], |
||
| 453 | EntityExistence::createForEntity( |
||
| 454 | $this->versionCommitDefinition->getEntityName(), |
||
| 455 | ['id' => Uuid::fromBytesToHex($commitId)], |
||
| 456 | ), |
||
| 457 | '' |
||
| 458 | ); |
||
| 459 | |||
| 460 | $commands = [$insert]; |
||
| 461 | |||
| 462 | foreach ($writtenEvents as $items) { |
||
| 463 | if (\count($items) === 0) { |
||
| 464 | continue; |
||
| 465 | } |
||
| 466 | |||
| 467 | $definition = $this->registry->getByEntityName($items[0]->getEntityName()); |
||
| 468 | $entityName = $definition->getEntityName(); |
||
| 469 | |||
| 470 | if (!$definition->isVersionAware()) { |
||
| 471 | continue; |
||
| 472 | } |
||
| 473 | |||
| 474 | if (mb_strpos('version', $entityName) === 0) { |
||
| 475 | continue; |
||
| 476 | } |
||
| 477 | |||
| 478 | /** @var EntityWriteResult $item */ |
||
| 479 | foreach ($items as $item) { |
||
| 480 | $payload = $item->getPayload(); |
||
| 481 | |||
| 482 | $primary = $item->getPrimaryKey(); |
||
| 483 | if (!\is_array($primary)) { |
||
| 484 | $primary = ['id' => $primary]; |
||
| 485 | } |
||
| 486 | $primary['versionId'] = $versionId; |
||
| 487 | |||
| 488 | $id = Uuid::randomBytes(); |
||
| 489 | |||
| 490 | $commands[] = new InsertCommand( |
||
| 491 | $this->versionCommitDataDefinition, |
||
| 492 | [ |
||
| 493 | 'id' => $id, |
||
| 494 | 'version_commit_id' => $commitId, |
||
| 495 | 'entity_name' => $entityName, |
||
| 496 | 'entity_id' => Json::encode($primary), |
||
| 497 | 'payload' => Json::encode($payload), |
||
| 498 | 'user_id' => $userId, |
||
| 499 | 'action' => $isClone ? 'clone' : $item->getOperation(), |
||
| 500 | 'created_at' => $date, |
||
| 501 | ], |
||
| 502 | ['id' => $id], |
||
| 503 | EntityExistence::createForEntity( |
||
| 504 | $this->versionCommitDataDefinition->getEntityName(), |
||
| 505 | ['id' => Uuid::fromBytesToHex($id)], |
||
| 506 | ), |
||
| 507 | '' |
||
| 508 | ); |
||
| 509 | } |
||
| 510 | } |
||
| 511 | |||
| 512 | if (\count($commands) <= 1) { |
||
| 513 | return; |
||
| 514 | } |
||
| 515 | |||
| 516 | $writeContext->scope(Context::SYSTEM_SCOPE, function () use ($commands, $writeContext): void { |
||
| 517 | $this->entityWriteGateway->execute($commands, $writeContext); |
||
| 518 | }); |
||
| 519 | } |
||
| 520 | |||
| 521 | /** |
||
| 522 | * @param array<string, array<string, mixed>|string|null> $payload |
||
| 523 | * |
||
| 524 | * @return array<string, array<string, mixed>|string|null> |
||
| 525 | */ |
||
| 526 | private function addVersionToPayload(array $payload, EntityDefinition $definition, string $versionId): array |
||
| 527 | { |
||
| 528 | $fields = $definition->getFields()->filter(fn (Field $field) => $field instanceof VersionField || $field instanceof ReferenceVersionField); |
||
| 529 | |||
| 530 | foreach ($fields as $field) { |
||
| 531 | $payload[$field->getPropertyName()] = $versionId; |
||
| 532 | } |
||
| 533 | |||
| 534 | return $payload; |
||
| 535 | } |
||
| 536 | |||
| 537 | /** |
||
| 538 | * @param array<string, array<string, mixed>|string|null> $nestedItem |
||
| 539 | * |
||
| 540 | * @return array<string, array<string, mixed>|string|null> |
||
| 541 | */ |
||
| 542 | private function removePrimaryKey(AssociationField $field, array $nestedItem): array |
||
| 543 | { |
||
| 544 | $pkFields = $field->getReferenceDefinition()->getPrimaryKeys(); |
||
| 545 | |||
| 546 | foreach ($pkFields as $pkField) { |
||
| 547 | /* |
||
| 548 | * `EntityTranslationDefinition`s dont have an `id`, they use a composite primary key consisting of the |
||
| 549 | * entity id and the `languageId`. When cloning the entity we want to copy the `languageId`. The entity id |
||
| 550 | * has to be unset, so that its set by the parent, resulting in a valid primary key. |
||
| 551 | */ |
||
| 552 | if ( |
||
| 553 | $field instanceof TranslationsAssociationField |
||
| 554 | && $pkField instanceof StorageAware |
||
| 555 | && $pkField->getStorageName() === $field->getLanguageField() |
||
| 556 | ) { |
||
| 557 | continue; |
||
| 558 | } |
||
| 559 | |||
| 560 | /** @var Field $pkField */ |
||
| 561 | if (\array_key_exists($pkField->getPropertyName(), $nestedItem)) { |
||
| 562 | unset($nestedItem[$pkField->getPropertyName()]); |
||
| 563 | } |
||
| 564 | } |
||
| 565 | |||
| 566 | return $nestedItem; |
||
| 567 | } |
||
| 568 | |||
| 569 | private function addCloneAssociations( |
||
| 570 | EntityDefinition $definition, |
||
| 571 | Criteria $criteria, |
||
| 572 | bool $cloneChildren, |
||
| 573 | int $childCounter = 1 |
||
| 574 | ): void { |
||
| 575 | // add all cascade delete associations |
||
| 576 | $cascades = $definition->getFields()->filter(function (Field $field) { |
||
| 577 | /** @var CascadeDelete|null $flag */ |
||
| 578 | $flag = $field->getFlag(CascadeDelete::class); |
||
| 579 | |||
| 580 | return $flag ? $flag->isCloneRelevant() : false; |
||
| 581 | }); |
||
| 582 | |||
| 583 | /** @var AssociationField $cascade */ |
||
| 584 | foreach ($cascades as $cascade) { |
||
| 585 | $nested = $criteria->getAssociation($cascade->getPropertyName()); |
||
| 586 | |||
| 587 | if ($cascade instanceof ManyToManyAssociationField) { |
||
| 588 | continue; |
||
| 589 | } |
||
| 590 | |||
| 591 | // many to one shouldn't be cascaded |
||
| 592 | if ($cascade instanceof ManyToOneAssociationField) { |
||
| 593 | continue; |
||
| 594 | } |
||
| 595 | |||
| 596 | $reference = $cascade->getReferenceDefinition(); |
||
| 597 | |||
| 598 | $childrenAware = $reference->isChildrenAware(); |
||
| 599 | |||
| 600 | // first level of parent-child tree? |
||
| 601 | if ($childrenAware && $reference !== $definition) { |
||
| 602 | // where product.children.parentId IS NULL |
||
| 603 | $nested->addFilter(new EqualsFilter($reference->getEntityName() . '.parentId', null)); |
||
| 604 | } |
||
| 605 | |||
| 606 | if ($cascade instanceof ChildrenAssociationField) { |
||
| 607 | // break endless loop |
||
| 608 | if ($childCounter >= 30 || !$cloneChildren) { |
||
| 609 | $criteria->removeAssociation($cascade->getPropertyName()); |
||
| 610 | |||
| 611 | continue; |
||
| 612 | } |
||
| 613 | |||
| 614 | ++$childCounter; |
||
| 615 | $this->addCloneAssociations($reference, $nested, $cloneChildren, $childCounter); |
||
| 616 | |||
| 617 | continue; |
||
| 618 | } |
||
| 619 | |||
| 620 | $this->addCloneAssociations($reference, $nested, $cloneChildren); |
||
| 621 | } |
||
| 622 | } |
||
| 623 | |||
| 624 | private function translationHasParent(VersionCommitEntity $commit, VersionCommitDataEntity $translationData): bool |
||
| 625 | { |
||
| 626 | /** @var EntityTranslationDefinition $translationDefinition */ |
||
| 627 | $translationDefinition = $this->registry->getByEntityName($translationData->getEntityName()); |
||
| 628 | |||
| 629 | $parentEntity = $translationDefinition->getParentDefinition()->getEntityName(); |
||
| 630 | |||
| 631 | $parentPropertyName = $this->getEntityForeignKeyName($parentEntity); |
||
| 632 | |||
| 633 | /** @var array<string, string> $payload */ |
||
| 634 | $payload = $translationData->getPayload(); |
||
| 635 | $parentId = $payload[$parentPropertyName]; |
||
| 636 | |||
| 637 | foreach ($commit->getData() as $data) { |
||
| 638 | if ($data->getEntityName() !== $parentEntity) { |
||
| 639 | continue; |
||
| 640 | } |
||
| 641 | |||
| 642 | $primary = $data->getEntityId(); |
||
| 643 | |||
| 644 | if (!isset($primary['id'])) { |
||
| 645 | continue; |
||
| 646 | } |
||
| 647 | |||
| 648 | if ($primary['id'] === $parentId) { |
||
| 649 | return true; |
||
| 650 | } |
||
| 651 | } |
||
| 652 | |||
| 653 | return false; |
||
| 654 | } |
||
| 655 | |||
| 656 | /** |
||
| 657 | * @param array<string> $entityId |
||
| 658 | * @param array<string|int, mixed> $payload |
||
| 659 | * |
||
| 660 | * @return array<string|int, mixed> |
||
| 661 | */ |
||
| 662 | private function addTranslationToPayload(array $entityId, array $payload, EntityDefinition $definition, VersionCommitEntity $commit): array |
||
| 663 | { |
||
| 664 | $translationDefinition = $definition->getTranslationDefinition(); |
||
| 665 | |||
| 666 | if (!$translationDefinition) { |
||
| 667 | return $payload; |
||
| 668 | } |
||
| 669 | if (!isset($entityId['id'])) { |
||
| 670 | return $payload; |
||
| 671 | } |
||
| 672 | |||
| 673 | $id = $entityId['id']; |
||
| 674 | |||
| 675 | $translations = []; |
||
| 676 | |||
| 677 | $foreignKeyName = $this->getEntityForeignKeyName($definition->getEntityName()); |
||
| 678 | |||
| 679 | foreach ($commit->getData() as $data) { |
||
| 680 | if ($data->getEntityName() !== $translationDefinition->getEntityName()) { |
||
| 681 | continue; |
||
| 682 | } |
||
| 683 | |||
| 684 | $translation = $data->getPayload(); |
||
| 685 | if (!isset($translation[$foreignKeyName])) { |
||
| 686 | continue; |
||
| 687 | } |
||
| 688 | |||
| 689 | if ($translation[$foreignKeyName] !== $id) { |
||
| 690 | continue; |
||
| 691 | } |
||
| 692 | |||
| 693 | $translations[] = $this->addVersionToPayload($translation, $translationDefinition, Defaults::LIVE_VERSION); |
||
| 694 | } |
||
| 695 | |||
| 696 | $payload['translations'] = $translations; |
||
| 697 | |||
| 698 | return $payload; |
||
| 699 | } |
||
| 700 | |||
| 701 | private function getEntityForeignKeyName(string $parentEntity): string |
||
| 702 | { |
||
| 703 | $parentPropertyName = explode('_', $parentEntity); |
||
| 704 | $parentPropertyName = array_map('ucfirst', $parentPropertyName); |
||
| 705 | |||
| 706 | return lcfirst(implode('', $parentPropertyName)) . 'Id'; |
||
| 707 | } |
||
| 708 | |||
| 709 | private function getCommits(string $versionId, WriteContext $writeContext): VersionCommitCollection |
||
| 710 | { |
||
| 711 | $criteria = new Criteria(); |
||
| 712 | $criteria->addFilter(new EqualsFilter('version_commit.versionId', $versionId)); |
||
| 713 | $criteria->addSorting(new FieldSorting('version_commit.autoIncrement')); |
||
| 714 | $commitIds = $this->entitySearcher->search($this->versionCommitDefinition, $criteria, $writeContext->getContext()); |
||
| 715 | |||
| 716 | $readCriteria = new Criteria(); |
||
| 717 | if ($commitIds->getTotal() > 0) { |
||
| 718 | $readCriteria = new Criteria($commitIds->getIds()); |
||
| 719 | } |
||
| 720 | |||
| 721 | $readCriteria->addAssociation('data'); |
||
| 722 | |||
| 723 | $readCriteria |
||
| 724 | ->getAssociation('data') |
||
| 725 | ->addSorting(new FieldSorting('autoIncrement')); |
||
| 726 | |||
| 727 | /** @var VersionCommitCollection $commits */ |
||
| 728 | $commits = $this->entityReader->read($this->versionCommitDefinition, $readCriteria, $writeContext->getContext()); |
||
| 729 | |||
| 730 | return $commits; |
||
| 731 | } |
||
| 732 | |||
| 733 | /** |
||
| 734 | * @return array{insert:array<string, array<int, mixed>>, update:array<string, array<int, mixed>>, delete:array<string, array<int, mixed>>} |
||
| 735 | */ |
||
| 736 | private function buildWrites(VersionCommitCollection $commits): array |
||
| 737 | { |
||
| 738 | $writes = [ |
||
| 739 | 'insert' => [], |
||
| 740 | 'update' => [], |
||
| 741 | 'delete' => [], |
||
| 742 | ]; |
||
| 743 | |||
| 744 | foreach ($commits as $commit) { |
||
| 745 | foreach ($commit->getData() as $data) { |
||
| 746 | $definition = $this->registry->getByEntityName($data->getEntityName()); |
||
| 747 | |||
| 748 | switch ($data->getAction()) { |
||
| 749 | case 'insert': |
||
| 750 | case 'update': |
||
| 751 | if ($definition instanceof EntityTranslationDefinition && $this->translationHasParent($commit, $data)) { |
||
| 752 | break; |
||
| 753 | } |
||
| 754 | |||
| 755 | $payload = $data->getPayload(); |
||
| 756 | if (empty($payload)) { |
||
| 757 | break; |
||
| 758 | } |
||
| 759 | $payload = $this->addVersionToPayload($payload, $definition, Defaults::LIVE_VERSION); |
||
| 760 | $payload = $this->addTranslationToPayload($data->getEntityId(), $payload, $definition, $commit); |
||
| 761 | $writes[$data->getAction()][$definition->getEntityName()][] = $payload; |
||
| 762 | |||
| 763 | break; |
||
| 764 | case 'delete': |
||
| 765 | $id = $data->getEntityId(); |
||
| 766 | $id = $this->addVersionToPayload($id, $definition, Defaults::LIVE_VERSION); |
||
| 767 | $writes['delete'][$definition->getEntityName()][] = $id; |
||
| 768 | |||
| 769 | break; |
||
| 770 | } |
||
| 771 | } |
||
| 772 | $writes['delete']['version_commit'][] = ['id' => $commit->getId()]; |
||
| 773 | } |
||
| 774 | |||
| 775 | return $writes; |
||
| 776 | } |
||
| 777 | |||
| 778 | /** |
||
| 779 | * @param array{insert:array<string, array<int, mixed>>, update:array<string, array<int, mixed>>, delete:array<string, array<int, mixed>>} $writes |
||
| 780 | */ |
||
| 781 | private function executeWrites(array $writes, WriteContext $liveContext): WriteResult |
||
| 782 | { |
||
| 783 | $operations = []; |
||
| 784 | foreach ($writes['insert'] as $entity => $payload) { |
||
| 785 | $operations[] = new SyncOperation('insert-' . $entity, $entity, 'upsert', $payload); |
||
| 786 | } |
||
| 787 | foreach ($writes['update'] as $entity => $payload) { |
||
| 788 | $operations[] = new SyncOperation('update-' . $entity, $entity, 'upsert', $payload); |
||
| 789 | } |
||
| 790 | foreach ($writes['delete'] as $entity => $payload) { |
||
| 791 | $operations[] = new SyncOperation('delete-' . $entity, $entity, 'delete', $payload); |
||
| 792 | } |
||
| 793 | |||
| 794 | return $this->entityWriter->sync($operations, $liveContext); |
||
| 795 | } |
||
| 796 | |||
| 797 | private function updateVersionData(VersionCommitCollection $commits, WriteContext $writeContext, string $versionId): void |
||
| 839 | } |
||
| 840 | |||
| 841 | private function deleteClones(VersionCommitCollection $commits, WriteContext $versionContext, string $versionId): void |
||
| 842 | { |
||
| 843 | $handled = []; |
||
| 844 | |||
| 845 | foreach ($commits as $commit) { |
||
| 864 | } |
||
| 865 | } |
||
| 866 | } |
||
| 867 | } |
||
| 868 |