Completed
Push — ezp-30882-thumbnail-strategy-i... ( de6b29...42b8c0 )
by
unknown
288:43 queued 276:39
created

DoctrineDatabase::internalLoadContent()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 86
c 0
b 0
f 0
cc 2
nc 2
nop 3
rs 8.3054

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