Completed
Push — ezp-31088-refactor-content-mod... ( ab3ba3 )
by
unknown
13:24 queued 33s
created

DoctrineDatabase::updateContent()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 24
nop 3
dl 0
loc 53
rs 8.0921
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\DBALException;
11
use Doctrine\DBAL\FetchMode;
12
use Doctrine\DBAL\ParameterType;
13
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
14
use eZ\Publish\API\Repository\Values\Content\Relation;
15
use eZ\Publish\Core\Base\Exceptions\BadStateException;
16
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
17
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder;
18
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
19
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
20
use eZ\Publish\Core\Persistence\Legacy\SharedGateway\Gateway as SharedGateway;
21
use eZ\Publish\SPI\Persistence\Content;
22
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
23
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
24
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
25
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
26
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
27
use eZ\Publish\SPI\Persistence\Content\Field;
28
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
29
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
30
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
31
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
32
use DOMXPath;
33
use DOMDocument;
34
use PDO;
35
36
/**
37
 * Doctrine database based content gateway.
38
 *
39
 * @internal Gateway implementation is considered internal. Use Persistence Content Handler instead.
40
 *
41
 * @see \eZ\Publish\SPI\Persistence\Content\Handler
42
 */
43
final class DoctrineDatabase extends Gateway
44
{
45
    /**
46
     * The native Doctrine connection.
47
     *
48
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
49
     *
50
     * @var \Doctrine\DBAL\Connection
51
     */
52
    protected $connection;
53
54
    /**
55
     * Query builder.
56
     *
57
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
58
     */
59
    protected $queryBuilder;
60
61
    /**
62
     * Caching language handler.
63
     *
64
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
65
     */
66
    protected $languageHandler;
67
68
    /**
69
     * Language mask generator.
70
     *
71
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
72
     */
73
    protected $languageMaskGenerator;
74
75
    /** @var \eZ\Publish\Core\Persistence\Legacy\SharedGateway\Gateway */
76
    private $sharedGateway;
77
78
    /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */
79
    private $databasePlatform;
80
81
    /**
82
     * @throws \Doctrine\DBAL\DBALException
83
     */
84
    public function __construct(
85
        Connection $connection,
86
        SharedGateway $sharedGateway,
87
        QueryBuilder $queryBuilder,
88
        LanguageHandler $languageHandler,
89
        LanguageMaskGenerator $languageMaskGenerator
90
    ) {
91
        $this->connection = $connection;
92
        $this->databasePlatform = $connection->getDatabasePlatform();
93
        $this->sharedGateway = $sharedGateway;
94
        $this->queryBuilder = $queryBuilder;
95
        $this->languageHandler = $languageHandler;
96
        $this->languageMaskGenerator = $languageMaskGenerator;
97
    }
98
99
    public function insertContentObject(CreateStruct $struct, int $currentVersionNo = 1): int
100
    {
101
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
102
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
103
104
        $name = $struct->name[$initialLanguageCode] ?? '';
105
106
        $query = $this->connection->createQueryBuilder();
107
        $query
108
            ->insert(self::CONTENT_ITEM_TABLE)
109
            ->values(
110
                [
111
                    'current_version' => $query->createPositionalParameter(
112
                        $currentVersionNo,
113
                        ParameterType::INTEGER
114
                    ),
115
                    'name' => $query->createPositionalParameter($name),
116
                    'contentclass_id' => $query->createPositionalParameter(
117
                        $struct->typeId,
118
                        ParameterType::INTEGER
119
                    ),
120
                    'section_id' => $query->createPositionalParameter(
121
                        $struct->sectionId,
122
                        ParameterType::INTEGER
123
                    ),
124
                    'owner_id' => $query->createPositionalParameter(
125
                        $struct->ownerId,
126
                        ParameterType::INTEGER
127
                    ),
128
                    'initial_language_id' => $query->createPositionalParameter(
129
                        $initialLanguageId,
130
                        ParameterType::INTEGER
131
                    ),
132
                    'remote_id' => $query->createPositionalParameter($struct->remoteId),
133
                    'modified' => $query->createPositionalParameter(0, ParameterType::INTEGER),
134
                    'published' => $query->createPositionalParameter(0, ParameterType::INTEGER),
135
                    'status' => $query->createPositionalParameter(
136
                        ContentInfo::STATUS_DRAFT,
137
                        ParameterType::INTEGER
138
                    ),
139
                    'language_mask' => $query->createPositionalParameter(
140
                        $this->languageMaskGenerator->generateLanguageMaskForFields(
141
                            $struct->fields,
142
                            $initialLanguageCode,
143
                            $struct->alwaysAvailable
144
                        ),
145
                        ParameterType::INTEGER
146
                    ),
147
                ]
148
            );
149
150
        $query->execute();
151
152
        return (int)$this->connection->lastInsertId(self::CONTENT_ITEM_SEQ);
153
    }
154
155
    public function insertVersion(VersionInfo $versionInfo, array $fields): int
156
    {
157
        $query = $this->connection->createQueryBuilder();
158
        $query
159
            ->insert(self::CONTENT_VERSION_TABLE)
160
            ->values(
161
                [
162
                    'version' => $query->createPositionalParameter(
163
                        $versionInfo->versionNo,
164
                        ParameterType::INTEGER
165
                    ),
166
                    'modified' => $query->createPositionalParameter(
167
                        $versionInfo->modificationDate,
168
                        ParameterType::INTEGER
169
                    ),
170
                    'creator_id' => $query->createPositionalParameter(
171
                        $versionInfo->creatorId,
172
                        ParameterType::INTEGER
173
                    ),
174
                    'created' => $query->createPositionalParameter(
175
                        $versionInfo->creationDate,
176
                        ParameterType::INTEGER
177
                    ),
178
                    'status' => $query->createPositionalParameter(
179
                        $versionInfo->status,
180
                        ParameterType::INTEGER
181
                    ),
182
                    'initial_language_id' => $query->createPositionalParameter(
183
                        $this->languageHandler->loadByLanguageCode(
184
                            $versionInfo->initialLanguageCode
185
                        )->id,
186
                        ParameterType::INTEGER
187
                    ),
188
                    'contentobject_id' => $query->createPositionalParameter(
189
                        $versionInfo->contentInfo->id,
190
                        ParameterType::INTEGER
191
                    ),
192
                    'language_mask' => $query->createPositionalParameter(
193
                        $this->languageMaskGenerator->generateLanguageMaskForFields(
194
                            $fields,
195
                            $versionInfo->initialLanguageCode,
196
                            $versionInfo->contentInfo->alwaysAvailable
197
                        ),
198
                        ParameterType::INTEGER
199
                    ),
200
                ]
201
            );
202
203
        $query->execute();
204
205
        return (int)$this->connection->lastInsertId(self::CONTENT_VERSION_SEQ);
206
    }
207
208
    public function updateContent(
209
        int $contentId,
210
        MetadataUpdateStruct $struct,
211
        ?VersionInfo $prePublishVersionInfo = null
212
    ): void {
213
        $query = $this->connection->createQueryBuilder();
214
        $query->update(self::CONTENT_ITEM_TABLE);
215
216
        $fieldsForUpdateMap = [
217
            'name' => ['value' => $struct->name, 'type' => ParameterType::STRING],
218
            'initial_language_id' => [
219
                'value' => $struct->mainLanguageId,
220
                'type' => ParameterType::INTEGER,
221
            ],
222
            'modified' => ['value' => $struct->modificationDate, 'type' => ParameterType::INTEGER],
223
            'owner_id' => ['value' => $struct->ownerId, 'type' => ParameterType::INTEGER],
224
            'published' => ['value' => $struct->publicationDate, 'type' => ParameterType::INTEGER],
225
            'remote_id' => ['value' => $struct->remoteId, 'type' => ParameterType::STRING],
226
            'is_hidden' => ['value' => $struct->isHidden, 'type' => ParameterType::BOOLEAN],
227
        ];
228
229
        foreach ($fieldsForUpdateMap as $fieldName => $field) {
230
            if (null === $field['value']) {
231
                continue;
232
            }
233
            $query->set(
234
                $fieldName,
235
                $query->createNamedParameter($field['value'], $field['type'], ":{$fieldName}")
236
            );
237
        }
238
239
        if ($prePublishVersionInfo !== null) {
240
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
241
                $prePublishVersionInfo->languageCodes,
242
                $struct->alwaysAvailable ?? $prePublishVersionInfo->contentInfo->alwaysAvailable
243
            );
244
            $query->set(
245
                'language_mask',
246
                $query->createNamedParameter($mask, ParameterType::INTEGER, ':languageMask')
247
            );
248
        }
249
250
        $query->where(
251
            $query->expr()->eq(
252
                'id',
253
                $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
254
            )
255
        );
256
257
        if (!empty($query->getQueryPart('set'))) {
258
            $query->execute();
259
        }
260
261
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
262
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
263
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
264
        }
265
    }
266
267
    /**
268
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
269
     *
270
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
271
     */
272
    public function updateVersion(int $contentId, int $versionNo, UpdateStruct $struct): void
273
    {
274
        $query = $this->connection->createQueryBuilder();
275
276
        $query
277
            ->update(self::CONTENT_VERSION_TABLE)
278
            ->set('creator_id', ':creator_id')
279
            ->set('modified', ':modified')
280
            ->set('initial_language_id', ':initial_language_id')
281
            ->set(
282
                'language_mask',
283
                $this->databasePlatform->getBitOrComparisonExpression(
284
                    'language_mask',
285
                    ':language_mask'
286
                )
287
            )
288
            ->setParameter('creator_id', $struct->creatorId, ParameterType::INTEGER)
289
            ->setParameter('modified', $struct->modificationDate, ParameterType::INTEGER)
290
            ->setParameter(
291
                'initial_language_id',
292
                $struct->initialLanguageId,
293
                ParameterType::INTEGER
294
            )
295
            ->setParameter(
296
                'language_mask',
297
                $this->languageMaskGenerator->generateLanguageMaskForFields(
298
                    $struct->fields,
299
                    $this->languageHandler->load($struct->initialLanguageId)->languageCode,
300
                    false
301
                ),
302
                ParameterType::INTEGER
303
            )
304
            ->where('contentobject_id = :content_id')
305
            ->andWhere('version = :version_no')
306
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
307
            ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
308
309
        $query->execute();
310
    }
311
312
    public function updateAlwaysAvailableFlag(int $contentId, ?bool $alwaysAvailable = null): void
313
    {
314
        // We will need to know some info on the current language mask to update the flag
315
        // everywhere needed
316
        $contentInfoRow = $this->loadContentInfo($contentId);
317
        $versionNo = (int)$contentInfoRow['current_version'];
318
        $languageMask = (int)$contentInfoRow['language_mask'];
319
        $initialLanguageId = (int)$contentInfoRow['initial_language_id'];
320
        if (!isset($alwaysAvailable)) {
321
            $alwaysAvailable = 1 === ($languageMask & 1);
322
        }
323
324
        $this->updateContentItemAlwaysAvailableFlag($contentId, $alwaysAvailable);
325
        $this->updateContentNameAlwaysAvailableFlag(
326
            $contentId,
327
            $versionNo,
328
            $alwaysAvailable
329
        );
330
        $this->updateContentFieldsAlwaysAvailableFlag(
331
            $contentId,
332
            $versionNo,
333
            $alwaysAvailable,
334
            $languageMask,
335
            $initialLanguageId
336
        );
337
    }
338
339
    private function updateContentItemAlwaysAvailableFlag(
340
        int $contentId,
341
        bool $alwaysAvailable
342
    ): void {
343
        $query = $this->connection->createQueryBuilder();
344
        $expr = $query->expr();
345
        $query
346
            ->update(self::CONTENT_ITEM_TABLE)
347
            ->set(
348
                'language_mask',
349
                $alwaysAvailable
350
                    ? $this->databasePlatform->getBitOrComparisonExpression(
351
                    'language_mask',
352
                    ':languageMaskOperand'
353
                )
354
                    : $this->databasePlatform->getBitAndComparisonExpression(
355
                    'language_mask',
356
                    ':languageMaskOperand'
357
                )
358
            )
359
            ->setParameter('languageMaskOperand', $alwaysAvailable ? 1 : -2)
360
            ->where(
361
                $expr->eq(
362
                    'id',
363
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
364
                )
365
            );
366
        $query->execute();
367
    }
368
369
    private function updateContentNameAlwaysAvailableFlag(
370
        int $contentId,
371
        int $versionNo,
372
        bool $alwaysAvailable
373
    ): void {
374
        $query = $this->connection->createQueryBuilder();
375
        $expr = $query->expr();
376
        $query
377
            ->update(self::CONTENT_NAME_TABLE)
378
            ->set(
379
                'language_id',
380
                $alwaysAvailable
381
                    ? $this->databasePlatform->getBitOrComparisonExpression(
382
                    'language_id',
383
                    ':languageMaskOperand'
384
                )
385
                    : $this->databasePlatform->getBitAndComparisonExpression(
386
                    'language_id',
387
                    ':languageMaskOperand'
388
                )
389
            )
390
            ->setParameter('languageMaskOperand', $alwaysAvailable ? 1 : -2)
391
            ->where(
392
                $expr->eq(
393
                    'contentobject_id',
394
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
395
                )
396
            )
397
            ->andWhere(
398
                $expr->eq(
399
                    'content_version',
400
                    $query->createNamedParameter($versionNo, ParameterType::INTEGER, ':versionNo')
401
                )
402
            );
403
        $query->execute();
404
    }
405
406
    private function updateContentFieldsAlwaysAvailableFlag(
407
        int $contentId,
408
        int $versionNo,
409
        bool $alwaysAvailable,
410
        int $languageMask,
411
        int $initialLanguageId
412
    ): void {
413
        $query = $this->connection->createQueryBuilder();
414
        $expr = $query->expr();
415
        $query
416
            ->update(self::CONTENT_FIELD_TABLE)
417
            ->where(
418
                $expr->eq(
419
                    'contentobject_id',
420
                    $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
421
                )
422
            )
423
            ->andWhere(
424
                $expr->eq(
425
                    'version',
426
                    $query->createNamedParameter($versionNo, ParameterType::INTEGER, ':versionNo')
427
                )
428
            );
429
430
        // If there is only a single language, update all fields and return
431
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($languageMask)) {
432
            $query
433
                ->set(
434
                    'language_id',
435
                    $alwaysAvailable
436
                        ? $this->databasePlatform->getBitOrComparisonExpression(
437
                        'language_id',
438
                        ':languageMaskOperand'
439
                    )
440
                        : $this->databasePlatform->getBitAndComparisonExpression(
441
                        'language_id',
442
                        ':languageMaskOperand'
443
                    )
444
                )
445
                ->setParameter('languageMaskOperand', $alwaysAvailable ? 1 : -2);
446
447
            $query->execute();
448
449
            return;
450
        }
451
452
        // Otherwise:
453
        // 1. Remove always available flag on all fields
454
        $query
455
            ->set(
456
                'language_id',
457
                $this->databasePlatform->getBitAndComparisonExpression(
458
                    'language_id',
459
                    ':languageMaskOperand'
460
                )
461
            )
462
            ->setParameter('languageMaskOperand', -2)
463
        ;
464
        $query->execute();
465
        $query->resetQueryPart('set');
466
467
        // 2. If Content is always available set the flag only on fields in main language
468
        if ($alwaysAvailable) {
469
            $query
470
                ->set(
471
                    'language_id',
472
                    $this->databasePlatform->getBitOrComparisonExpression(
473
                        'language_id',
474
                        ':languageMaskOperand'
475
                    )
476
                )
477
                ->setParameter('languageMaskOperand', $alwaysAvailable ? 1 : -2);
478
479
            $query->andWhere(
480
                $expr->gt(
481
                    $this->databasePlatform->getBitAndComparisonExpression(
482
                        'language_id',
483
                        $query->createNamedParameter($initialLanguageId, ParameterType::INTEGER, ':initialLanguageId')
484
                    ),
485
                    $query->createNamedParameter(0, ParameterType::INTEGER, ':zero')
486
                )
487
            );
488
            $query->execute();
489
        }
490
    }
491
492
    public function setStatus(int $contentId, int $version, int $status): bool
493
    {
494
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
495
            $query = $this->queryBuilder->getSetVersionStatusQuery($contentId, $version, $status);
496
            $rowCount = $query->execute();
497
498
            return $rowCount > 0;
499
        } else {
500
            // If the version's status is PUBLISHED, we use dedicated method for publishing
501
            $this->setPublishedStatus($contentId, $version);
502
503
            return true;
504
        }
505
    }
506
507
    public function setPublishedStatus(int $contentId, int $versionNo): void
508
    {
509
        $query = $this->queryBuilder->getSetVersionStatusQuery(
510
            $contentId,
511
            $versionNo,
512
            VersionInfo::STATUS_PUBLISHED
513
        );
514
515
        /* this part allows set status `published` only if there is no other published version of the content */
516
        $notExistPublishedVersion = <<< HEREDOC
517
            NOT EXISTS (
518
                SELECT 1 FROM (
519
                    SELECT 1 FROM ezcontentobject_version  WHERE contentobject_id = :contentId AND status = :status 
520
                ) as V
521
            )
522
HEREDOC;
523
524
        $query->andWhere($notExistPublishedVersion);
525
        if (0 === $query->execute()) {
526
            throw new BadStateException(
527
                '$contentId', "Someone just published another version of Content item {$contentId}"
528
            );
529
        }
530
        $this->markContentAsPublished($contentId, $versionNo);
531
    }
532
533
    private function markContentAsPublished(int $contentId, int $versionNo): void
534
    {
535
        $query = $this->connection->createQueryBuilder();
536
        $query
537
            ->update('ezcontentobject')
538
            ->set('status', ':status')
539
            ->set('current_version', ':versionNo')
540
            ->where('id =:contentId')
541
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
542
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER)
543
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
544
        $query->execute();
545
    }
546
547
    /**
548
     * @return int ID
549
     *
550
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
551
     */
552
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value): int
553
    {
554
        $query = $this->connection->createQueryBuilder();
555
556
        $this->setInsertFieldValues($query, $content, $field, $value);
557
558
        // Insert with auto increment ID
559
        $nextId = $this->sharedGateway->getColumnNextIntegerValue(
560
            self::CONTENT_FIELD_TABLE,
561
            'id',
562
            self::CONTENT_FIELD_SEQ
563
        );
564
        // avoid trying to insert NULL to trigger default column value behavior
565
        if (null !== $nextId) {
566
            $query
567
                ->setValue('id', ':field_id')
568
                ->setParameter('field_id', $nextId, ParameterType::INTEGER);
569
        }
570
571
        $query->execute();
572
573
        return (int)$this->sharedGateway->getLastInsertedId(self::CONTENT_FIELD_SEQ);
574
    }
575
576
    public function insertExistingField(
577
        Content $content,
578
        Field $field,
579
        StorageFieldValue $value
580
    ): void {
581
        $query = $this->connection->createQueryBuilder();
582
583
        $this->setInsertFieldValues($query, $content, $field, $value);
584
585
        $query
586
            ->setValue('id', ':field_id')
587
            ->setParameter('field_id', $field->id, ParameterType::INTEGER);
588
589
        $query->execute();
590
    }
591
592
    /**
593
     * Set the given query field (ezcontentobject_attribute) values.
594
     *
595
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
596
     */
597
    private function setInsertFieldValues(
598
        DoctrineQueryBuilder $query,
599
        Content $content,
600
        Field $field,
601
        StorageFieldValue $value
602
    ): void {
603
        $query
604
            ->insert(self::CONTENT_FIELD_TABLE)
605
            ->values(
606
                [
607
                    'contentobject_id' => ':content_id',
608
                    'contentclassattribute_id' => ':field_definition_id',
609
                    'data_type_string' => ':data_type_string',
610
                    'language_code' => ':language_code',
611
                    'version' => ':version_no',
612
                    'data_float' => ':data_float',
613
                    'data_int' => ':data_int',
614
                    'data_text' => ':data_text',
615
                    'sort_key_int' => ':sort_key_int',
616
                    'sort_key_string' => ':sort_key_string',
617
                    'language_id' => ':language_id',
618
                ]
619
            )
620
            ->setParameter(
621
                'content_id',
622
                $content->versionInfo->contentInfo->id,
623
                ParameterType::INTEGER
624
            )
625
            ->setParameter('field_definition_id', $field->fieldDefinitionId, ParameterType::INTEGER)
626
            ->setParameter('data_type_string', $field->type, ParameterType::STRING)
627
            ->setParameter('language_code', $field->languageCode, ParameterType::STRING)
628
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER)
629
            ->setParameter('data_float', $value->dataFloat)
630
            ->setParameter('data_int', $value->dataInt, ParameterType::INTEGER)
631
            ->setParameter('data_text', $value->dataText, ParameterType::STRING)
632
            ->setParameter('sort_key_int', $value->sortKeyInt, ParameterType::INTEGER)
633
            ->setParameter(
634
                'sort_key_string',
635
                mb_substr((string)$value->sortKeyString, 0, 255),
636
                ParameterType::STRING
637
            )
638
            ->setParameter(
639
                'language_id',
640
                $this->languageMaskGenerator->generateLanguageIndicator(
641
                    $field->languageCode,
642
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
643
                ),
644
                ParameterType::INTEGER
645
            );
646
    }
647
648
    /**
649
     * Check if $languageCode is always available in $content.
650
     */
651
    private function isLanguageAlwaysAvailable(Content $content, string $languageCode): bool
652
    {
653
        return
654
            $content->versionInfo->contentInfo->alwaysAvailable &&
655
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
656
        ;
657
    }
658
659
    public function updateField(Field $field, StorageFieldValue $value): void
660
    {
661
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
662
        // cannot change on update
663
        $query = $this->connection->createQueryBuilder();
664
        $this->setFieldUpdateValues($query, $value);
665
        $query
666
            ->where('id = :field_id')
667
            ->andWhere('version = :version_no')
668
            ->setParameter('field_id', $field->id, ParameterType::INTEGER)
669
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER);
670
671
        $query->execute();
672
    }
673
674
    /**
675
     * Set update fields on $query based on $value.
676
     */
677
    private function setFieldUpdateValues(
678
        DoctrineQueryBuilder $query,
679
        StorageFieldValue $value
680
    ): void {
681
        $query
682
            ->update(self::CONTENT_FIELD_TABLE)
683
            ->set('data_float', ':data_float')
684
            ->set('data_int', ':data_int')
685
            ->set('data_text', ':data_text')
686
            ->set('sort_key_int', ':sort_key_int')
687
            ->set('sort_key_string', ':sort_key_string')
688
            ->setParameter('data_float', $value->dataFloat)
689
            ->setParameter('data_int', $value->dataInt, ParameterType::INTEGER)
690
            ->setParameter('data_text', $value->dataText, ParameterType::STRING)
691
            ->setParameter('sort_key_int', $value->sortKeyInt, ParameterType::INTEGER)
692
            ->setParameter('sort_key_string', mb_substr((string)$value->sortKeyString, 0, 255))
693
        ;
694
    }
695
696
    /**
697
     * Update an existing, non-translatable field.
698
     */
699
    public function updateNonTranslatableField(
700
        Field $field,
701
        StorageFieldValue $value,
702
        int $contentId
703
    ): void {
704
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
705
        // cannot change on update
706
        $query = $this->connection->createQueryBuilder();
707
        $this->setFieldUpdateValues($query, $value);
708
        $query
709
            ->where('contentclassattribute_id = :field_definition_id')
710
            ->andWhere('contentobject_id = :content_id')
711
            ->andWhere('version = :version_no')
712
            ->setParameter('field_definition_id', $field->fieldDefinitionId, ParameterType::INTEGER)
713
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
714
            ->setParameter('version_no', $field->versionNo, ParameterType::INTEGER);
715
716
        $query->execute();
717
    }
718
719
    public function load(int $contentId, ?int $version = null, ?array $translations = null): array
720
    {
721
        return $this->internalLoadContent([$contentId], $version, $translations);
722
    }
723
724
    public function loadContentList(array $contentIds, ?array $translations = null): array
725
    {
726
        return $this->internalLoadContent($contentIds, null, $translations);
727
    }
728
729
    /**
730
     * Build query for the <code>load</code> and <code>loadContentList</code> methods.
731
     *
732
     * @param int[] $contentIds
733
     * @param string[]|null $translations a list of language codes
734
     *
735
     * @see load(), loadContentList()
736
     */
737
    private function internalLoadContent(
738
        array $contentIds,
739
        ?int $version = null,
740
        ?array $translations = null
741
    ): array {
742
        $queryBuilder = $this->connection->createQueryBuilder();
743
        $expr = $queryBuilder->expr();
744
        $queryBuilder
745
            ->select(
746
                'c.id AS ezcontentobject_id',
747
                'c.contentclass_id AS ezcontentobject_contentclass_id',
748
                'c.section_id AS ezcontentobject_section_id',
749
                'c.owner_id AS ezcontentobject_owner_id',
750
                'c.remote_id AS ezcontentobject_remote_id',
751
                'c.current_version AS ezcontentobject_current_version',
752
                'c.initial_language_id AS ezcontentobject_initial_language_id',
753
                'c.modified AS ezcontentobject_modified',
754
                'c.published AS ezcontentobject_published',
755
                'c.status AS ezcontentobject_status',
756
                'c.name AS ezcontentobject_name',
757
                'c.language_mask AS ezcontentobject_language_mask',
758
                'c.is_hidden AS ezcontentobject_is_hidden',
759
                'v.id AS ezcontentobject_version_id',
760
                'v.version AS ezcontentobject_version_version',
761
                'v.modified AS ezcontentobject_version_modified',
762
                'v.creator_id AS ezcontentobject_version_creator_id',
763
                'v.created AS ezcontentobject_version_created',
764
                'v.status AS ezcontentobject_version_status',
765
                'v.language_mask AS ezcontentobject_version_language_mask',
766
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
767
                'a.id AS ezcontentobject_attribute_id',
768
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
769
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
770
                'a.language_code AS ezcontentobject_attribute_language_code',
771
                'a.language_id AS ezcontentobject_attribute_language_id',
772
                'a.data_float AS ezcontentobject_attribute_data_float',
773
                'a.data_int AS ezcontentobject_attribute_data_int',
774
                'a.data_text AS ezcontentobject_attribute_data_text',
775
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
776
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
777
                't.main_node_id AS ezcontentobject_tree_main_node_id'
778
            )
779
            ->from('ezcontentobject', 'c')
780
            ->innerJoin(
781
                'c',
782
                'ezcontentobject_version',
783
                'v',
784
                $expr->andX(
785
                    $expr->eq('c.id', 'v.contentobject_id'),
786
                    $expr->eq('v.version', $version ?? 'c.current_version')
787
                )
788
            )
789
            ->innerJoin(
790
                'v',
791
                'ezcontentobject_attribute',
792
                'a',
793
                $expr->andX(
794
                    $expr->eq('v.contentobject_id', 'a.contentobject_id'),
795
                    $expr->eq('v.version', 'a.version')
796
                )
797
            )
798
            ->leftJoin(
799
                'c',
800
                'ezcontentobject_tree',
801
                't',
802
                $expr->andX(
803
                    $expr->eq('c.id', 't.contentobject_id'),
804
                    $expr->eq('t.node_id', 't.main_node_id')
805
                )
806
            );
807
808
        $queryBuilder->where(
809
            $expr->in(
810
                'c.id',
811
                $queryBuilder->createNamedParameter($contentIds, Connection::PARAM_INT_ARRAY)
812
            )
813
        );
814
815
        if (!empty($translations)) {
816
            $queryBuilder->andWhere(
817
                $expr->in(
818
                    'a.language_code',
819
                    $queryBuilder->createNamedParameter($translations, Connection::PARAM_STR_ARRAY)
820
                )
821
            );
822
        }
823
824
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
825
    }
826
827
    public function loadContentInfo(int $contentId): array
828
    {
829
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
830
        $queryBuilder
831
            ->where('c.id = :id')
832
            ->setParameter('id', $contentId, ParameterType::INTEGER);
833
834
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
835
        if (empty($results)) {
836
            throw new NotFound('content', "id: $contentId");
837
        }
838
839
        return $results[0];
840
    }
841
842
    public function loadContentInfoList(array $contentIds): array
843
    {
844
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
845
        $queryBuilder
846
            ->where('c.id IN (:ids)')
847
            ->setParameter('ids', $contentIds, Connection::PARAM_INT_ARRAY);
848
849
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
850
    }
851
852
    public function loadContentInfoByRemoteId(string $remoteId): array
853
    {
854
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder();
855
        $queryBuilder
856
            ->where('c.remote_id = :id')
857
            ->setParameter('id', $remoteId, ParameterType::STRING);
858
859
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
860
        if (empty($results)) {
861
            throw new NotFound('content', "remote_id: $remoteId");
862
        }
863
864
        return $results[0];
865
    }
866
867
    public function loadContentInfoByLocationId(int $locationId): array
868
    {
869
        $queryBuilder = $this->queryBuilder->createLoadContentInfoQueryBuilder(false);
870
        $queryBuilder
871
            ->where('t.node_id = :id')
872
            ->setParameter('id', $locationId, ParameterType::INTEGER);
873
874
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
875
        if (empty($results)) {
876
            throw new NotFound('content', "node_id: $locationId");
877
        }
878
879
        return $results[0];
880
    }
881
882
    public function loadVersionInfo(int $contentId, ?int $versionNo = null): array
883
    {
884
        $queryBuilder = $this->queryBuilder->createVersionInfoFindQueryBuilder();
885
        $expr = $queryBuilder->expr();
886
887
        $queryBuilder
888
            ->where(
889
                $expr->eq(
890
                    'v.contentobject_id',
891
                    $queryBuilder->createNamedParameter(
892
                        $contentId,
893
                        ParameterType::INTEGER,
894
                        ':content_id'
895
                    )
896
                )
897
            );
898
899
        if (null !== $versionNo) {
900
            $queryBuilder
901
                ->andWhere(
902
                    $expr->eq(
903
                        'v.version',
904
                        $queryBuilder->createNamedParameter(
905
                            $versionNo,
906
                            ParameterType::INTEGER,
907
                            ':version_no'
908
                        )
909
                    )
910
                );
911
        } else {
912
            $queryBuilder->andWhere($expr->eq('v.version', 'c.current_version'));
913
        }
914
915
        return $queryBuilder->execute()->fetchAll(PDO::FETCH_ASSOC);
916
    }
917
918
    public function countVersionsForUser(int $userId, int $status = VersionInfo::STATUS_DRAFT): int
919
    {
920
        $query = $this->connection->createQueryBuilder();
921
        $expr = $query->expr();
922
        $query
923
            ->select($this->databasePlatform->getCountExpression('v.id'))
924
            ->from('ezcontentobject_version', 'v')
925
            ->innerJoin(
926
                'v',
927
                'ezcontentobject',
928
                'c',
929
                $expr->andX(
930
                    $expr->eq('c.id', 'v.contentobject_id'),
931
                    $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
932
                )
933
            )
934
            ->where(
935
                $query->expr()->andX(
936
                    $query->expr()->eq('v.status', ':status'),
937
                    $query->expr()->eq('v.creator_id', ':user_id')
938
                )
939
            )
940
            ->setParameter(':status', $status, \PDO::PARAM_INT)
941
            ->setParameter(':user_id', $userId, \PDO::PARAM_INT);
942
943
        return (int) $query->execute()->fetchColumn();
944
    }
945
946
    /**
947
     * Return data for all versions with the given status created by the given $userId.
948
     *
949
     * @return string[][]
950
     */
951
    public function listVersionsForUser(int $userId, int $status = VersionInfo::STATUS_DRAFT): array
952
    {
953
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
954
        $query
955
            ->where('v.status = :status')
956
            ->andWhere('v.creator_id = :user_id')
957
            ->setParameter('status', $status, ParameterType::INTEGER)
958
            ->setParameter('user_id', $userId, ParameterType::INTEGER)
959
            ->orderBy('v.id');
960
961
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
962
    }
963
964
    public function loadVersionsForUser(
965
        int $userId,
966
        int $status = VersionInfo::STATUS_DRAFT,
967
        int $offset = 0,
968
        int $limit = -1
969
    ): array {
970
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
971
        $expr = $query->expr();
972
        $query->where(
973
            $expr->andX(
974
                $expr->eq('v.status', ':status'),
975
                $expr->eq('v.creator_id', ':user_id'),
976
                $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
977
            )
978
        )
979
        ->setFirstResult($offset)
980
        ->setParameter(':status', $status, \PDO::PARAM_INT)
981
        ->setParameter(':user_id', $userId, \PDO::PARAM_INT);
982
983
        if ($limit > 0) {
984
            $query->setMaxResults($limit);
985
        }
986
987
        $query->orderBy('v.modified', 'DESC');
988
        $query->addOrderBy('v.id', 'DESC');
989
990
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
991
    }
992
993
    public function listVersions(int $contentId, ?int $status = null, int $limit = -1): array
994
    {
995
        $query = $this->queryBuilder->createVersionInfoFindQueryBuilder();
996
        $query
997
            ->where('v.contentobject_id = :content_id')
998
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
999
1000
        if ($status !== null) {
1001
            $query
1002
                ->andWhere('v.status = :status')
1003
                ->setParameter('status', $status);
1004
        }
1005
1006
        if ($limit > 0) {
1007
            $query->setMaxResults($limit);
1008
        }
1009
1010
        $query->orderBy('v.id');
1011
1012
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1013
    }
1014
1015
    /**
1016
     * @return int[]
1017
     */
1018
    public function listVersionNumbers(int $contentId): array
1019
    {
1020
        $query = $this->connection->createQueryBuilder();
1021
        $query
1022
            ->select('version')
1023
            ->from(self::CONTENT_VERSION_TABLE)
1024
            ->where('contentobject_id = :contentId')
1025
            ->groupBy('version')
1026
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
1027
1028
        return array_map('intval', $query->execute()->fetchAll(FetchMode::COLUMN));
1029
    }
1030
1031
    public function getLastVersionNumber(int $contentId): int
1032
    {
1033
        $query = $this->connection->createQueryBuilder();
1034
        $query
1035
            ->select($this->databasePlatform->getMaxExpression('version'))
1036
            ->from(self::CONTENT_VERSION_TABLE)
1037
            ->where('contentobject_id = :content_id')
1038
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1039
1040
        $statement = $query->execute();
1041
1042
        return (int)$statement->fetchColumn();
1043
    }
1044
1045
    /**
1046
     * @return int[]
1047
     */
1048
    public function getAllLocationIds(int $contentId): array
1049
    {
1050
        $query = $this->connection->createQueryBuilder();
1051
        $query
1052
            ->select('node_id')
1053
            ->from('ezcontentobject_tree')
1054
            ->where('contentobject_id = :content_id')
1055
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1056
1057
        $statement = $query->execute();
1058
1059
        return $statement->fetchAll(FetchMode::COLUMN);
1060
    }
1061
1062
    /**
1063
     * @return int[][]
1064
     */
1065
    public function getFieldIdsByType(
1066
        int $contentId,
1067
        ?int $versionNo = null,
1068
        ?string $languageCode = null
1069
    ): array {
1070
        $query = $this->connection->createQueryBuilder();
1071
        $query
1072
            ->select('id', 'data_type_string')
1073
            ->from(self::CONTENT_FIELD_TABLE)
1074
            ->where('contentobject_id = :content_id')
1075
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1076
1077
        if (null !== $versionNo) {
1078
            $query
1079
                ->andWhere('version = :version_no')
1080
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1081
        }
1082
1083
        if (!empty($languageCode)) {
1084
            $query
1085
                ->andWhere('language_code = :language_code')
1086
                ->setParameter('language_code', $languageCode, ParameterType::STRING);
1087
        }
1088
1089
        $statement = $query->execute();
1090
1091
        $result = [];
1092
        foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
1093
            if (!isset($result[$row['data_type_string']])) {
1094
                $result[$row['data_type_string']] = [];
1095
            }
1096
            $result[$row['data_type_string']][] = (int)$row['id'];
1097
        }
1098
1099
        return $result;
1100
    }
1101
1102
    public function deleteRelations(int $contentId, ?int $versionNo = null): void
1103
    {
1104
        $query = $this->connection->createQueryBuilder();
1105
        $query
1106
            ->delete(self::CONTENT_RELATION_TABLE)
1107
            ->where('from_contentobject_id = :content_id')
1108
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1109
1110
        if (null !== $versionNo) {
1111
            $query
1112
                ->andWhere('from_contentobject_version = :version_no')
1113
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1114
        } else {
1115
            $query->orWhere('to_contentobject_id = :content_id');
1116
        }
1117
1118
        $query->execute();
1119
    }
1120
1121
    public function removeReverseFieldRelations(int $contentId): void
1122
    {
1123
        $query = $this->connection->createQueryBuilder();
1124
        $expr = $query->expr();
1125
        $query
1126
            ->select(['a.id', 'a.version', 'a.data_type_string', 'a.data_text'])
1127
            ->from(self::CONTENT_FIELD_TABLE, 'a')
1128
            ->innerJoin(
1129
                'a',
1130
                'ezcontentobject_link',
1131
                'l',
1132
                $expr->andX(
1133
                    'l.from_contentobject_id = a.contentobject_id',
1134
                    'l.from_contentobject_version = a.version',
1135
                    'l.contentclassattribute_id = a.contentclassattribute_id'
1136
                )
1137
            )
1138
            ->where('l.to_contentobject_id = :content_id')
1139
            ->andWhere(
1140
                $expr->gt(
1141
                    $this->databasePlatform->getBitAndComparisonExpression(
1142
                        'l.relation_type',
1143
                        ':relation_type'
1144
                    ),
1145
                    0
1146
                )
1147
            )
1148
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1149
            ->setParameter('relation_type', Relation::FIELD, ParameterType::INTEGER);
1150
1151
        $statement = $query->execute();
1152
1153
        while ($row = $statement->fetch(FetchMode::ASSOCIATIVE)) {
1154
            if ($row['data_type_string'] === 'ezobjectrelation') {
1155
                $this->removeRelationFromRelationField($row);
1156
            }
1157
1158
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1159
                $this->removeRelationFromRelationListField($contentId, $row);
1160
            }
1161
        }
1162
    }
1163
1164
    /**
1165
     * Update field value of RelationList field type identified by given $row data,
1166
     * removing relations toward given $contentId.
1167
     *
1168
     * @param array $row
1169
     */
1170
    private function removeRelationFromRelationListField(int $contentId, array $row): void
1171
    {
1172
        $document = new DOMDocument('1.0', 'utf-8');
1173
        $document->loadXML($row['data_text']);
1174
1175
        $xpath = new DOMXPath($document);
1176
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1177
1178
        $relationItems = $xpath->query($xpathExpression);
1179
        foreach ($relationItems as $relationItem) {
1180
            $relationItem->parentNode->removeChild($relationItem);
1181
        }
1182
1183
        $query = $this->connection->createQueryBuilder();
1184
        $query
1185
            ->update(self::CONTENT_FIELD_TABLE)
1186
            ->set('data_text', ':data_text')
1187
            ->setParameter('data_text', $document->saveXML(), ParameterType::STRING)
1188
            ->where('id = :attribute_id')
1189
            ->andWhere('version = :version_no')
1190
            ->setParameter('attribute_id', (int)$row['id'], ParameterType::INTEGER)
1191
            ->setParameter('version_no', (int)$row['version'], ParameterType::INTEGER);
1192
1193
        $query->execute();
1194
    }
1195
1196
    /**
1197
     * Update field value of Relation field type identified by given $row data,
1198
     * removing relation data.
1199
     *
1200
     * @param array $row
1201
     */
1202
    private function removeRelationFromRelationField(array $row): void
1203
    {
1204
        $query = $this->connection->createQueryBuilder();
1205
        $query
1206
            ->update(self::CONTENT_FIELD_TABLE)
1207
            ->set('data_int', ':data_int')
1208
            ->set('sort_key_int', ':sort_key_int')
1209
            ->setParameter('data_int', null, ParameterType::NULL)
1210
            ->setParameter('sort_key_int', 0, ParameterType::INTEGER)
1211
            ->where('id = :attribute_id')
1212
            ->andWhere('version = :version_no')
1213
            ->setParameter('attribute_id', (int)$row['id'], ParameterType::INTEGER)
1214
            ->setParameter('version_no', (int)$row['version'], ParameterType::INTEGER);
1215
1216
        $query->execute();
1217
    }
1218
1219
    public function deleteField(int $fieldId): void
1220
    {
1221
        $query = $this->connection->createQueryBuilder();
1222
        $query
1223
            ->delete(self::CONTENT_FIELD_TABLE)
1224
            ->where('id = :field_id')
1225
            ->setParameter('field_id', $fieldId, ParameterType::INTEGER)
1226
        ;
1227
1228
        $query->execute();
1229
    }
1230
1231
    public function deleteFields(int $contentId, ?int $versionNo = null): void
1232
    {
1233
        $query = $this->connection->createQueryBuilder();
1234
        $query
1235
            ->delete(self::CONTENT_FIELD_TABLE)
1236
            ->where('contentobject_id = :content_id')
1237
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1238
1239
        if (null !== $versionNo) {
1240
            $query
1241
                ->andWhere('version = :version_no')
1242
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1243
        }
1244
1245
        $query->execute();
1246
    }
1247
1248
    public function deleteVersions(int $contentId, ?int $versionNo = null): void
1249
    {
1250
        $query = $this->connection->createQueryBuilder();
1251
        $query
1252
            ->delete(self::CONTENT_VERSION_TABLE)
1253
            ->where('contentobject_id = :content_id')
1254
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1255
1256
        if (null !== $versionNo) {
1257
            $query
1258
                ->andWhere('version = :version_no')
1259
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1260
        }
1261
1262
        $query->execute();
1263
    }
1264
1265
    public function deleteNames(int $contentId, int $versionNo = null): void
1266
    {
1267
        $query = $this->connection->createQueryBuilder();
1268
        $query
1269
            ->delete(self::CONTENT_NAME_TABLE)
1270
            ->where('contentobject_id = :content_id')
1271
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1272
1273
        if (isset($versionNo)) {
1274
            $query
1275
                ->andWhere('content_version = :version_no')
1276
                ->setParameter('version_no', $versionNo, ParameterType::INTEGER);
1277
        }
1278
1279
        $query->execute();
1280
    }
1281
1282
    /**
1283
     * Query Content name table to find if a name record for the given parameters exists.
1284
     */
1285
    private function contentNameExists(int $contentId, int $version, string $languageCode): bool
1286
    {
1287
        $query = $this->connection->createQueryBuilder();
1288
        $query
1289
            ->select($this->databasePlatform->getCountExpression('contentobject_id'))
1290
            ->from(self::CONTENT_NAME_TABLE)
1291
            ->where('contentobject_id = :content_id')
1292
            ->andWhere('content_version = :version_no')
1293
            ->andWhere('content_translation = :language_code')
1294
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1295
            ->setParameter('version_no', $version, ParameterType::INTEGER)
1296
            ->setParameter('language_code', $languageCode, ParameterType::STRING);
1297
1298
        $stmt = $query->execute();
1299
1300
        return (int)$stmt->fetch(FetchMode::COLUMN) > 0;
1301
    }
1302
1303
    public function setName(int $contentId, int $version, string $name, string $languageCode): void
1304
    {
1305
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1306
1307
        $query = $this->connection->createQueryBuilder();
1308
1309
        // prepare parameters
1310
        $query
1311
            ->setParameter('name', $name, ParameterType::STRING)
1312
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1313
            ->setParameter('version_no', $version, ParameterType::INTEGER)
1314
            ->setParameter('language_id', $language->id, ParameterType::INTEGER)
1315
            ->setParameter('language_code', $language->languageCode, ParameterType::STRING)
1316
        ;
1317
1318
        if (!$this->contentNameExists($contentId, $version, $language->languageCode)) {
1319
            $query
1320
                ->insert(self::CONTENT_NAME_TABLE)
1321
                ->values(
1322
                    [
1323
                        'contentobject_id' => ':content_id',
1324
                        'content_version' => ':version_no',
1325
                        'content_translation' => ':language_code',
1326
                        'name' => ':name',
1327
                        'language_id' => $this->getSetNameLanguageMaskSubQuery(),
1328
                        'real_translation' => ':language_code',
1329
                    ]
1330
                );
1331
        } else {
1332
            $query
1333
                ->update(self::CONTENT_NAME_TABLE)
1334
                ->set('name', ':name')
1335
                ->set('language_id', $this->getSetNameLanguageMaskSubQuery())
1336
                ->set('real_translation', ':language_code')
1337
                ->where('contentobject_id = :content_id')
1338
                ->andWhere('content_version = :version_no')
1339
                ->andWhere('content_translation = :language_code');
1340
        }
1341
1342
        $query->execute();
1343
    }
1344
1345
    /**
1346
     * Return a language sub select query for setName.
1347
     *
1348
     * The query generates the proper language mask at the runtime of the INSERT/UPDATE query
1349
     * generated by setName.
1350
     *
1351
     * @see setName
1352
     */
1353
    private function getSetNameLanguageMaskSubQuery(): string
1354
    {
1355
        return <<<SQL
1356
(SELECT
1357
    CASE
1358
        WHEN (initial_language_id = :language_id AND (language_mask & :language_id) <> 0 )
1359
        THEN (:language_id | 1)
1360
        ELSE :language_id 
1361
    END
1362
    FROM ezcontentobject
1363
    WHERE id = :content_id)
1364
SQL;
1365
    }
1366
1367
    public function deleteContent(int $contentId): void
1368
    {
1369
        $query = $this->connection->createQueryBuilder();
1370
        $query
1371
            ->delete(self::CONTENT_ITEM_TABLE)
1372
            ->where('id = :content_id')
1373
            ->setParameter('content_id', $contentId, ParameterType::INTEGER)
1374
        ;
1375
1376
        $query->execute();
1377
    }
1378
1379
    public function loadRelations(
1380
        int $contentId,
1381
        ?int $contentVersionNo = null,
1382
        ?int $relationType = null
1383
    ): array {
1384
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1385
        $expr = $query->expr();
1386
        $query
1387
            ->innerJoin(
1388
                'l',
1389
                'ezcontentobject',
1390
                'ezcontentobject_to',
1391
                $expr->andX(
1392
                    'l.to_contentobject_id = ezcontentobject_to.id',
1393
                    'ezcontentobject_to.status = :status'
1394
                )
1395
            )
1396
            ->where(
1397
                'l.from_contentobject_id = :content_id'
1398
            )
1399
            ->setParameter(
1400
                'status',
1401
                ContentInfo::STATUS_PUBLISHED,
1402
                ParameterType::INTEGER
1403
            )
1404
            ->setParameter('content_id', $contentId, ParameterType::INTEGER);
1405
1406
        // source version number
1407
        if (null !== $contentVersionNo) {
1408
            $query
1409
                ->andWhere('l.from_contentobject_version = :version_no')
1410
                ->setParameter('version_no', $contentVersionNo, ParameterType::INTEGER);
1411
        } else {
1412
            // from published version only
1413
            $query
1414
                ->innerJoin(
1415
                    'ezcontentobject_to',
1416
                    'ezcontentobject',
1417
                    'c',
1418
                    $expr->andX(
1419
                        'c.id = l.from_contentobject_id',
1420
                        'c.current_version = l.from_contentobject_version'
1421
                    )
1422
                );
1423
        }
1424
1425
        // relation type
1426
        if (null !== $relationType) {
1427
            $query
1428
                ->andWhere(
1429
                    $expr->gt(
1430
                        $this->databasePlatform->getBitAndComparisonExpression(
1431
                            'l.relation_type',
1432
                            ':relation_type'
1433
                        ),
1434
                        0
1435
                    )
1436
                )
1437
                ->setParameter('relation_type', $relationType, ParameterType::INTEGER);
1438
        }
1439
1440
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1441
    }
1442
1443
    public function countReverseRelations(int $toContentId, ?int $relationType = null): int
1444
    {
1445
        $query = $this->connection->createQueryBuilder();
1446
        $expr = $query->expr();
1447
        $query
1448
            ->select($this->databasePlatform->getCountExpression('l.id'))
1449
            ->from(self::CONTENT_RELATION_TABLE, 'l')
1450
            ->innerJoin(
1451
                'l',
1452
                'ezcontentobject',
1453
                'c',
1454
                $expr->andX(
1455
                    $expr->eq('l.from_contentobject_id', 'c.id'),
1456
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
1457
                    $expr->eq('c.status', ':status')
1458
                )
1459
            )
1460
            ->where(
1461
                $expr->eq('l.to_contentobject_id', ':to_content_id')
1462
            )
1463
            ->setParameter('to_content_id', $toContentId, ParameterType::INTEGER)
1464
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
1465
        ;
1466
1467
        // relation type
1468
        if ($relationType !== null) {
1469
            $query->andWhere(
1470
                $expr->gt(
1471
                    $this->databasePlatform->getBitAndComparisonExpression(
1472
                        'l.relation_type',
1473
                        $relationType
1474
                    ),
1475
                    0
1476
                )
1477
            );
1478
        }
1479
1480
        return (int)$query->execute()->fetchColumn();
1481
    }
1482
1483
    public function loadReverseRelations(int $toContentId, ?int $relationType = null): array
1484
    {
1485
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1486
        $expr = $query->expr();
1487
        $query
1488
            ->join(
1489
                'l',
1490
                'ezcontentobject',
1491
                'c',
1492
                $expr->andX(
1493
                    'c.id = l.from_contentobject_id',
1494
                    'c.current_version = l.from_contentobject_version',
1495
                    'c.status = :status'
1496
                )
1497
            )
1498
            ->where('l.to_contentobject_id = :to_content_id')
1499
            ->setParameter('to_content_id', $toContentId, ParameterType::INTEGER)
1500
            ->setParameter(
1501
                'status',
1502
                ContentInfo::STATUS_PUBLISHED,
1503
                ParameterType::INTEGER
1504
            );
1505
1506
        // relation type
1507
        if (null !== $relationType) {
1508
            $query->andWhere(
1509
                $expr->gt(
1510
                    $this->databasePlatform->getBitAndComparisonExpression(
1511
                        'l.relation_type',
1512
                        ':relation_type'
1513
                    ),
1514
                    0
1515
                )
1516
            )
1517
                ->setParameter('relation_type', $relationType, ParameterType::INTEGER);
1518
        }
1519
1520
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1521
    }
1522
1523
    public function listReverseRelations(
1524
        int $toContentId,
1525
        int $offset = 0,
1526
        int $limit = -1,
1527
        ?int $relationType = null
1528
    ): array {
1529
        $query = $this->queryBuilder->createRelationFindQueryBuilder();
1530
        $expr = $query->expr();
1531
        $query
1532
            ->innerJoin(
1533
                'l',
1534
                'ezcontentobject',
1535
                'c',
1536
                $expr->andX(
1537
                    $expr->eq('l.from_contentobject_id', 'c.id'),
1538
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
1539
                    $expr->eq('c.status', ContentInfo::STATUS_PUBLISHED)
1540
                )
1541
            )
1542
            ->where(
1543
                $expr->eq('l.to_contentobject_id', ':toContentId')
1544
            )
1545
            ->setParameter(':toContentId', $toContentId, ParameterType::INTEGER);
1546
1547
        // relation type
1548
        if ($relationType !== null) {
1549
            $query->andWhere(
1550
                $expr->gt(
1551
                    $this->databasePlatform->getBitAndComparisonExpression(
1552
                        'l.relation_type',
1553
                        $relationType
1554
                    ),
1555
                    0
1556
                )
1557
            );
1558
        }
1559
        $query->setFirstResult($offset);
1560
        if ($limit > 0) {
1561
            $query->setMaxResults($limit);
1562
        }
1563
        $query->orderBy('l.id', 'DESC');
1564
1565
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1566
    }
1567
1568
    public function insertRelation(RelationCreateStruct $createStruct): int
1569
    {
1570
        $query = $this->connection->createQueryBuilder();
1571
        $query
1572
            ->insert(self::CONTENT_RELATION_TABLE)
1573
            ->values(
1574
                [
1575
                    'contentclassattribute_id' => ':field_definition_id',
1576
                    'from_contentobject_id' => ':from_content_id',
1577
                    'from_contentobject_version' => ':from_version_no',
1578
                    'relation_type' => ':relation_type',
1579
                    'to_contentobject_id' => ':to_content_id',
1580
                ]
1581
            )
1582
            ->setParameter(
1583
                'field_definition_id',
1584
                (int)$createStruct->sourceFieldDefinitionId,
1585
                ParameterType::INTEGER
1586
            )
1587
            ->setParameter(
1588
                'from_content_id',
1589
                $createStruct->sourceContentId,
1590
                ParameterType::INTEGER
1591
            )
1592
            ->setParameter(
1593
                'from_version_no',
1594
                $createStruct->sourceContentVersionNo,
1595
                ParameterType::INTEGER
1596
            )
1597
            ->setParameter('relation_type', $createStruct->type, ParameterType::INTEGER)
1598
            ->setParameter(
1599
                'to_content_id',
1600
                $createStruct->destinationContentId,
1601
                ParameterType::INTEGER
1602
            );
1603
1604
        $query->execute();
1605
1606
        return (int)$this->connection->lastInsertId(self::CONTENT_RELATION_SEQ);
1607
    }
1608
1609
    public function deleteRelation(int $relationId, int $type): void
1610
    {
1611
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
1612
        // existing relation type by given $relationId for comparison
1613
        $query = $this->connection->createQueryBuilder();
1614
        $query
1615
            ->select('relation_type')
1616
            ->from(self::CONTENT_RELATION_TABLE)
1617
            ->where('id = :relation_id')
1618
            ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1619
        ;
1620
1621
        $loadedRelationType = $query->execute()->fetchColumn();
1622
1623
        if (!$loadedRelationType) {
1624
            return;
1625
        }
1626
1627
        $query = $this->connection->createQueryBuilder();
1628
        // If relation type matches then delete
1629
        if (((int)$loadedRelationType) === ((int)$type)) {
1630
            $query
1631
                ->delete(self::CONTENT_RELATION_TABLE)
1632
                ->where('id = :relation_id')
1633
                ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1634
            ;
1635
1636
            $query->execute();
1637
        } elseif ($loadedRelationType & $type) {
1638
            // If relation type is composite update bitmask
1639
1640
            $query
1641
                ->update(self::CONTENT_RELATION_TABLE)
1642
                ->set(
1643
                    'relation_type',
1644
                    // make & operation removing given $type from the bitmask
1645
                    $this->databasePlatform->getBitAndComparisonExpression(
1646
                        'relation_type',
1647
                        ':relation_type'
1648
                    )
1649
                )
1650
                // set the relation type as needed for the above & expression
1651
                ->setParameter('relation_type', ~$type, ParameterType::INTEGER)
1652
                ->where('id = :relation_id')
1653
                ->setParameter('relation_id', $relationId, ParameterType::INTEGER)
1654
            ;
1655
1656
            $query->execute();
1657
        }
1658
    }
1659
1660
    /**
1661
     * @return int[]
1662
     */
1663
    public function getContentIdsByContentTypeId(int $contentTypeId): array
1664
    {
1665
        $query = $this->connection->createQueryBuilder();
1666
        $query
1667
            ->select('id')
1668
            ->from(self::CONTENT_ITEM_TABLE)
1669
            ->where('contentclass_id = :content_type_id')
1670
            ->setParameter('content_type_id', $contentTypeId, ParameterType::INTEGER);
1671
1672
        $statement = $query->execute();
1673
1674
        return array_map('intval', $statement->fetchAll(FetchMode::COLUMN));
1675
    }
1676
1677
    public function loadVersionedNameData(array $rows): array
1678
    {
1679
        $query = $this->queryBuilder->createNamesQuery();
1680
        $expr = $query->expr();
1681
        $conditions = [];
1682
        foreach ($rows as $row) {
1683
            $conditions[] = $expr->andX(
1684
                $expr->eq(
1685
                    'contentobject_id',
1686
                    $query->createPositionalParameter($row['id'], ParameterType::INTEGER)
1687
                ),
1688
                $expr->eq(
1689
                    'content_version',
1690
                    $query->createPositionalParameter($row['version'], ParameterType::INTEGER)
1691
                ),
1692
            );
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected ')'
Loading history...
1693
        }
1694
1695
        $query->where($expr->orX(...$conditions));
1696
1697
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1698
    }
1699
1700
    /**
1701
     * @throws \Doctrine\DBAL\DBALException
1702
     */
1703
    public function copyRelations(
1704
        int $originalContentId,
1705
        int $copiedContentId,
1706
        ?int $versionNo = null
1707
    ): void {
1708
        $selectQuery = $this->connection->createQueryBuilder();
1709
        $selectQuery
1710
            ->select(
1711
                'l.contentclassattribute_id',
1712
                ':copied_id',
1713
                'l.from_contentobject_version',
1714
                'l.relation_type',
1715
                'l.to_contentobject_id'
1716
            )
1717
            ->from(self::CONTENT_RELATION_TABLE, 'l')
1718
            ->where('l.from_contentobject_id = :original_id')
1719
            ->setParameter('copied_id', $copiedContentId, ParameterType::INTEGER)
1720
            ->setParameter('original_id', $originalContentId, ParameterType::INTEGER);
1721
1722
        if ($versionNo) {
1723
            $selectQuery
1724
                ->andWhere('l.from_contentobject_version = :version')
1725
                ->setParameter(':version', $versionNo, ParameterType::INTEGER);
1726
        }
1727
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
1728
        $insertQuery = <<<SQL
1729
INSERT INTO ezcontentobject_link (
1730
    contentclassattribute_id, 
1731
    from_contentobject_id, 
1732
    from_contentobject_version, 
1733
    relation_type, 
1734
    to_contentobject_id 
1735
)
1736
SQL;
1737
1738
        $insertQuery .= $selectQuery->getSQL();
1739
1740
        $this->connection->executeUpdate(
1741
            $insertQuery,
1742
            $selectQuery->getParameters(),
1743
            $selectQuery->getParameterTypes()
1744
        );
1745
    }
1746
1747
    /**
1748
     * {@inheritdoc}
1749
     *
1750
     * @throws \Doctrine\DBAL\ConnectionException
1751
     * @throws \Doctrine\DBAL\DBALException
1752
     */
1753
    public function deleteTranslationFromContent(int $contentId, string $languageCode): void
1754
    {
1755
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1756
1757
        $this->connection->beginTransaction();
1758
        try {
1759
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
1760
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
1761
            $this->deleteTranslationFromContentObject($contentId, $language->id);
1762
1763
            $this->connection->commit();
1764
        } catch (DBALException $e) {
1765
            $this->connection->rollBack();
1766
            throw $e;
1767
        }
1768
    }
1769
1770
    public function deleteTranslatedFields(
1771
        string $languageCode,
1772
        int $contentId,
1773
        ?int $versionNo = null
1774
    ): void {
1775
        $query = $this->connection->createQueryBuilder();
1776
        $query
1777
            ->delete('ezcontentobject_attribute')
1778
            ->where('contentobject_id = :contentId')
1779
            ->andWhere('language_code = :languageCode')
1780
            ->setParameters(
1781
                [
1782
                    ':contentId' => $contentId,
1783
                    ':languageCode' => $languageCode,
1784
                ]
1785
            )
1786
        ;
1787
1788
        if (null !== $versionNo) {
1789
            $query
1790
                ->andWhere('version = :versionNo')
1791
                ->setParameter(':versionNo', $versionNo)
1792
            ;
1793
        }
1794
1795
        $query->execute();
1796
    }
1797
1798
    /**
1799
     * {@inheritdoc}
1800
     *
1801
     * @throws \Doctrine\DBAL\DBALException
1802
     */
1803
    public function deleteTranslationFromVersion(
1804
        int $contentId,
1805
        int $versionNo,
1806
        string $languageCode
1807
    ): void {
1808
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
1809
1810
        $this->connection->beginTransaction();
1811
        try {
1812
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
1813
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
1814
1815
            $this->connection->commit();
1816
        } catch (DBALException $e) {
1817
            $this->connection->rollBack();
1818
            throw $e;
1819
        }
1820
    }
1821
1822
    /**
1823
     * Delete translation from the ezcontentobject_name table.
1824
     *
1825
     * @param int $versionNo optional, if specified, apply to this Version only.
1826
     */
1827
    private function deleteTranslationFromContentNames(
1828
        int $contentId,
1829
        string $languageCode,
1830
        ?int $versionNo = null
1831
    ) {
1832
        $query = $this->connection->createQueryBuilder();
1833
        $query
1834
            ->delete('ezcontentobject_name')
1835
            ->where('contentobject_id=:contentId')
1836
            ->andWhere('real_translation=:languageCode')
1837
            ->setParameters(
1838
                [
1839
                    ':languageCode' => $languageCode,
1840
                    ':contentId' => $contentId,
1841
                ]
1842
            )
1843
        ;
1844
1845
        if (null !== $versionNo) {
1846
            $query
1847
                ->andWhere('content_version = :versionNo')
1848
                ->setParameter(':versionNo', $versionNo)
1849
            ;
1850
        }
1851
1852
        $query->execute();
1853
    }
1854
1855
    /**
1856
     * Remove language from language_mask of ezcontentobject.
1857
     *
1858
     * @param int $contentId
1859
     * @param int $languageId
1860
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
1861
     */
1862
    private function deleteTranslationFromContentObject($contentId, $languageId)
1863
    {
1864
        $query = $this->connection->createQueryBuilder();
1865
        $query->update('ezcontentobject')
1866
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1867
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
1868
            ->set('modified', ':now')
1869
            ->where('id = :contentId')
1870
            ->andWhere(
1871
            // make sure removed translation is not the last one (incl. alwaysAvailable)
1872
                $query->expr()->andX(
1873
                    'language_mask & ~ ' . $languageId . ' <> 0',
1874
                    'language_mask & ~ ' . $languageId . ' <> 1'
1875
                )
1876
            )
1877
            ->setParameter(':now', time())
1878
            ->setParameter(':contentId', $contentId)
1879
        ;
1880
1881
        $rowCount = $query->execute();
1882
1883
        // no rows updated means that most likely somehow it was the last remaining translation
1884
        if ($rowCount === 0) {
1885
            throw new BadStateException(
1886
                '$languageCode',
1887
                'The provided translation is the only translation in this version'
1888
            );
1889
        }
1890
    }
1891
1892
    /**
1893
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
1894
     * if it matches the removed one.
1895
     *
1896
     * @param int|null $versionNo optional, if specified, apply to this Version only.
1897
     *
1898
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1899
     */
1900
    private function deleteTranslationFromContentVersions(
1901
        int $contentId,
1902
        int $languageId,
1903
        ?int $versionNo = null
1904
    ) {
1905
        $query = $this->connection->createQueryBuilder();
1906
        $query->update('ezcontentobject_version')
1907
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
1908
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
1909
            ->set('modified', ':now')
1910
            // update initial_language_id only if it matches removed translation languageId
1911
            ->set(
1912
                'initial_language_id',
1913
                'CASE WHEN initial_language_id = :languageId ' .
1914
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
1915
                'ELSE initial_language_id END'
1916
            )
1917
            ->where('contentobject_id = :contentId')
1918
            ->andWhere(
1919
            // make sure removed translation is not the last one (incl. alwaysAvailable)
1920
                $query->expr()->andX(
1921
                    'language_mask & ~ ' . $languageId . ' <> 0',
1922
                    'language_mask & ~ ' . $languageId . ' <> 1'
1923
                )
1924
            )
1925
            ->setParameter(':now', time())
1926
            ->setParameter(':contentId', $contentId)
1927
            ->setParameter(':languageId', $languageId)
1928
        ;
1929
1930
        if (null !== $versionNo) {
1931
            $query
1932
                ->andWhere('version = :versionNo')
1933
                ->setParameter(':versionNo', $versionNo)
1934
            ;
1935
        }
1936
1937
        $rowCount = $query->execute();
1938
1939
        // no rows updated means that most likely somehow it was the last remaining translation
1940
        if ($rowCount === 0) {
1941
            throw new BadStateException(
1942
                '$languageCode',
1943
                'The provided translation is the only translation in this version'
1944
            );
1945
        }
1946
    }
1947
}
1948