Completed
Push — ezp-31088-replace-content-gw-d... ( e5a0f6 )
by
unknown
17:33 queued 03:34
created

DoctrineDatabase::updateAlwaysAvailableFlag()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

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