Completed
Push — apply-code-style ( 1a43bf...37cc85 )
by
unknown
45:48
created

DoctrineDatabase::updateContent()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * File containing the DoctrineDatabase Content Gateway class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\DBALException;
13
use Doctrine\DBAL\FetchMode;
14
use Doctrine\DBAL\ParameterType;
15
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
16
use eZ\Publish\Core\Base\Exceptions\BadStateException;
17
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
18
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder;
19
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
20
use eZ\Publish\Core\Persistence\Database\UpdateQuery;
21
use eZ\Publish\Core\Persistence\Database\InsertQuery;
22
use eZ\Publish\Core\Persistence\Database\SelectQuery;
23
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
24
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
25
use eZ\Publish\SPI\Persistence\Content;
26
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
27
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
28
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
29
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
30
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
31
use eZ\Publish\SPI\Persistence\Content\Field;
32
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
33
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
34
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
35
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
36
use DOMXPath;
37
use DOMDocument;
38
use PDO;
39
40
/**
41
 * Doctrine database based content gateway.
42
 */
43
class DoctrineDatabase extends Gateway
44
{
45
    /**
46
     * eZ Doctrine database handler.
47
     *
48
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
49
     *
50
     * @deprecated Start to use DBAL $connection instead.
51
     */
52
    protected $dbHandler;
53
54
    /**
55
     * The native Doctrine connection.
56
     *
57
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
58
     *
59
     * @var \Doctrine\DBAL\Connection
60
     */
61
    protected $connection;
62
63
    /**
64
     * Query builder.
65
     *
66
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
67
     */
68
    protected $queryBuilder;
69
70
    /**
71
     * Caching language handler.
72
     *
73
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
74
     */
75
    protected $languageHandler;
76
77
    /**
78
     * Language mask generator.
79
     *
80
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
81
     */
82
    protected $languageMaskGenerator;
83
84
    /**
85
     * Creates a new gateway based on $db.
86
     *
87
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $db
88
     * @param \Doctrine\DBAL\Connection $connection
89
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder $queryBuilder
90
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
91
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
92
     */
93
    public function __construct(
94
        DatabaseHandler $db,
95
        Connection $connection,
96
        QueryBuilder $queryBuilder,
97
        LanguageHandler $languageHandler,
98
        LanguageMaskGenerator $languageMaskGenerator
99
    ) {
100
        $this->dbHandler = $db;
101
        $this->connection = $connection;
102
        $this->queryBuilder = $queryBuilder;
103
        $this->languageHandler = $languageHandler;
0 ignored issues
show
Documentation Bug introduced by
$languageHandler is of type object<eZ\Publish\SPI\Pe...ntent\Language\Handler>, but the property $languageHandler was declared to be of type object<eZ\Publish\Core\P...anguage\CachingHandler>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
104
        $this->languageMaskGenerator = $languageMaskGenerator;
105
    }
106
107
    /**
108
     * Get context definition for external storage layers.
109
     *
110
     * @return array
111
     */
112
    public function getContext()
113
    {
114
        return [
115
            'identifier' => 'LegacyStorage',
116
            'connection' => $this->dbHandler,
117
        ];
118
    }
119
120
    /**
121
     * Inserts a new content object.
122
     *
123
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct
124
     * @param mixed $currentVersionNo
125
     *
126
     * @return int ID
127
     */
128
    public function insertContentObject(CreateStruct $struct, $currentVersionNo = 1)
129
    {
130
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
131
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
132
133
        if (isset($struct->name[$initialLanguageCode])) {
134
            $name = $struct->name[$initialLanguageCode];
135
        } else {
136
            $name = '';
137
        }
138
139
        $q = $this->dbHandler->createInsertQuery();
140
        $q->insertInto(
141
            $this->dbHandler->quoteTable('ezcontentobject')
142
        )->set(
143
            $this->dbHandler->quoteColumn('id'),
144
            $this->dbHandler->getAutoIncrementValue('ezcontentobject', 'id')
145
        )->set(
146
            $this->dbHandler->quoteColumn('current_version'),
147
            $q->bindValue($currentVersionNo, null, \PDO::PARAM_INT)
148
        )->set(
149
            $this->dbHandler->quoteColumn('name'),
150
            $q->bindValue($name, null, \PDO::PARAM_STR)
151
        )->set(
152
            $this->dbHandler->quoteColumn('contentclass_id'),
153
            $q->bindValue($struct->typeId, null, \PDO::PARAM_INT)
154
        )->set(
155
            $this->dbHandler->quoteColumn('section_id'),
156
            $q->bindValue($struct->sectionId, null, \PDO::PARAM_INT)
157
        )->set(
158
            $this->dbHandler->quoteColumn('owner_id'),
159
            $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
160
        )->set(
161
            $this->dbHandler->quoteColumn('initial_language_id'),
162
            $q->bindValue($initialLanguageId, null, \PDO::PARAM_INT)
163
        )->set(
164
            $this->dbHandler->quoteColumn('remote_id'),
165
            $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
166
        )->set(
167
            $this->dbHandler->quoteColumn('modified'),
168
            $q->bindValue(0, null, \PDO::PARAM_INT)
169
        )->set(
170
            $this->dbHandler->quoteColumn('published'),
171
            $q->bindValue(0, null, \PDO::PARAM_INT)
172
        )->set(
173
            $this->dbHandler->quoteColumn('status'),
174
            $q->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
175
        )->set(
176
            $this->dbHandler->quoteColumn('language_mask'),
177
            $q->bindValue(
178
                $this->generateLanguageMask(
179
                    $struct->fields,
180
                    $initialLanguageCode,
181
                    $struct->alwaysAvailable
182
                ),
183
                null,
184
                \PDO::PARAM_INT
185
            )
186
        );
187
188
        $q->prepare()->execute();
189
190
        return (int)$this->dbHandler->lastInsertId(
191
            $this->dbHandler->getSequenceName('ezcontentobject', 'id')
192
        );
193
    }
194
195
    /**
196
     * Generates a language mask for $fields.
197
     *
198
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
199
     * @param string $initialLanguageCode
200
     * @param bool $isAlwaysAvailable
201
     *
202
     * @return int
203
     */
204
    protected function generateLanguageMask(array $fields, string $initialLanguageCode, bool $isAlwaysAvailable): int
205
    {
206
        $languages = [$initialLanguageCode => true];
207
        foreach ($fields as $field) {
208
            if (isset($languages[$field->languageCode])) {
209
                continue;
210
            }
211
212
            $languages[$field->languageCode] = true;
213
        }
214
215
        return $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(array_keys($languages), $isAlwaysAvailable);
216
    }
217
218
    /**
219
     * Inserts a new version.
220
     *
221
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $versionInfo
222
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
223
     *
224
     * @return int ID
225
     */
226
    public function insertVersion(VersionInfo $versionInfo, array $fields)
227
    {
228
        /** @var $q \eZ\Publish\Core\Persistence\Database\InsertQuery */
229
        $q = $this->dbHandler->createInsertQuery();
230
        $q->insertInto(
231
            $this->dbHandler->quoteTable('ezcontentobject_version')
232
        )->set(
233
            $this->dbHandler->quoteColumn('id'),
234
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_version', 'id')
235
        )->set(
236
            $this->dbHandler->quoteColumn('version'),
237
            $q->bindValue($versionInfo->versionNo, null, \PDO::PARAM_INT)
238
        )->set(
239
            $this->dbHandler->quoteColumn('modified'),
240
            $q->bindValue($versionInfo->modificationDate, null, \PDO::PARAM_INT)
241
        )->set(
242
            $this->dbHandler->quoteColumn('creator_id'),
243
            $q->bindValue($versionInfo->creatorId, null, \PDO::PARAM_INT)
244
        )->set(
245
            $this->dbHandler->quoteColumn('created'),
246
            $q->bindValue($versionInfo->creationDate, null, \PDO::PARAM_INT)
247
        )->set(
248
            $this->dbHandler->quoteColumn('status'),
249
            $q->bindValue($versionInfo->status, null, \PDO::PARAM_INT)
250
        )->set(
251
            $this->dbHandler->quoteColumn('initial_language_id'),
252
            $q->bindValue(
253
                $this->languageHandler->loadByLanguageCode($versionInfo->initialLanguageCode)->id,
254
                null,
255
                \PDO::PARAM_INT
256
            )
257
        )->set(
258
            $this->dbHandler->quoteColumn('contentobject_id'),
259
            $q->bindValue($versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
260
        )->set(
261
            // As described in field mapping document
262
            $this->dbHandler->quoteColumn('workflow_event_pos'),
263
            $q->bindValue(0, null, \PDO::PARAM_INT)
264
        )->set(
265
            $this->dbHandler->quoteColumn('language_mask'),
266
            $q->bindValue(
267
                $this->generateLanguageMask(
268
                    $fields,
269
                    $versionInfo->initialLanguageCode,
270
                    $versionInfo->contentInfo->alwaysAvailable
271
                ),
272
                null,
273
                \PDO::PARAM_INT
274
            )
275
        );
276
277
        $q->prepare()->execute();
278
279
        return (int)$this->dbHandler->lastInsertId(
280
            $this->dbHandler->getSequenceName('ezcontentobject_version', 'id')
281
        );
282
    }
283
284
    /**
285
     * Updates an existing content identified by $contentId in respect to $struct.
286
     *
287
     * @param int $contentId
288
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $struct
289
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $prePublishVersionInfo Provided on publish
290
     *
291
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
292
     */
293
    public function updateContent($contentId, MetadataUpdateStruct $struct, VersionInfo $prePublishVersionInfo = null)
294
    {
295
        $query = $this->connection->createQueryBuilder();
296
        $query->update('ezcontentobject');
297
298
        $fieldsForUpdateMap = [
299
            'name' => ['value' => $struct->name, 'type' => ParameterType::STRING],
300
            'initial_language_id' => ['value' => $struct->mainLanguageId, 'type' => ParameterType::INTEGER],
301
            'modified' => ['value' => $struct->modificationDate, 'type' => ParameterType::INTEGER],
302
            'owner_id' => ['value' => $struct->ownerId, 'type' => ParameterType::INTEGER],
303
            'published' => ['value' => $struct->publicationDate, 'type' => ParameterType::INTEGER],
304
            'remote_id' => ['value' => $struct->remoteId, 'type' => ParameterType::STRING],
305
            'is_hidden' => ['value' => $struct->isHidden, 'type' => ParameterType::BOOLEAN],
306
        ];
307
308
        foreach ($fieldsForUpdateMap as $fieldName => $field) {
309
            if (null === $field['value']) {
310
                continue;
311
            }
312
            $query->set(
313
                $fieldName,
314
                $query->createNamedParameter($field['value'], $field['type'], ":{$fieldName}")
315
            );
316
        }
317
318
        if ($prePublishVersionInfo !== null) {
319
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
320
                $prePublishVersionInfo->languageCodes,
321
                $struct->alwaysAvailable ?? $prePublishVersionInfo->contentInfo->alwaysAvailable
322
            );
323
324
            $query->set(
325
                'language_mask',
326
                $query->createNamedParameter($mask, ParameterType::INTEGER, ':languageMask')
327
            );
328
        }
329
330
        $query->where(
331
            $query->expr()->eq(
332
                'id',
333
                $query->createNamedParameter($contentId, ParameterType::INTEGER, ':contentId')
334
            )
335
        );
336
337
        if (!empty($query->getQueryPart('set'))) {
338
            $query->execute();
339
        }
340
341
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
342
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
343
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
344
        }
345
    }
346
347
    /**
348
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
349
     *
350
     * @param int $contentId
351
     * @param int $versionNo
352
     * @param \eZ\Publish\SPI\Persistence\Content\UpdateStruct $struct
353
     */
354
    public function updateVersion($contentId, $versionNo, UpdateStruct $struct)
355
    {
356
        $q = $this->dbHandler->createUpdateQuery();
357
        $q->update(
358
            $this->dbHandler->quoteTable('ezcontentobject_version')
359
        )->set(
360
            $this->dbHandler->quoteColumn('creator_id'),
361
            $q->bindValue($struct->creatorId, null, \PDO::PARAM_INT)
362
        )->set(
363
            $this->dbHandler->quoteColumn('modified'),
364
            $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
365
        )->set(
366
            $this->dbHandler->quoteColumn('initial_language_id'),
367
            $q->bindValue($struct->initialLanguageId, null, \PDO::PARAM_INT)
368
        )->set(
369
            $this->dbHandler->quoteColumn('language_mask'),
370
            $q->expr->bitOr(
371
                $this->dbHandler->quoteColumn('language_mask'),
372
                $q->bindValue(
373
                    $this->generateLanguageMask(
374
                        $struct->fields,
375
                        $this->languageHandler->load($struct->initialLanguageId)->languageCode,
376
                        false
377
                    ),
378
                    null,
379
                    \PDO::PARAM_INT
380
                )
381
            )
382
        )->where(
383
            $q->expr->lAnd(
384
                $q->expr->eq(
385
                    $this->dbHandler->quoteColumn('contentobject_id'),
386
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
387
                ),
388
                $q->expr->eq(
389
                    $this->dbHandler->quoteColumn('version'),
390
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
391
                )
392
            )
393
        );
394
        $q->prepare()->execute();
395
    }
396
397
    /**
398
     * Updates "always available" flag for Content identified by $contentId, in respect to
399
     * Content's current main language and optionally new $alwaysAvailable state.
400
     *
401
     * @param int $contentId
402
     * @param bool|null $alwaysAvailable New "always available" value or null if not defined
403
     */
404
    public function updateAlwaysAvailableFlag($contentId, $alwaysAvailable = null)
405
    {
406
        // We will need to know some info on the current language mask to update the flag
407
        // everywhere needed
408
        $contentInfoRow = $this->loadContentInfo($contentId);
409
        if (!isset($alwaysAvailable)) {
410
            $alwaysAvailable = 1 === ($contentInfoRow['language_mask'] & 1);
411
        }
412
413
        /** @var $q \eZ\Publish\Core\Persistence\Database\UpdateQuery */
414
        $q = $this->dbHandler->createUpdateQuery();
415
        $q
416
            ->update($this->dbHandler->quoteTable('ezcontentobject'))
417
            ->set(
418
                $this->dbHandler->quoteColumn('language_mask'),
419
                $alwaysAvailable ?
420
                    $q->expr->bitOr($this->dbHandler->quoteColumn('language_mask'), 1) :
421
                    $q->expr->bitAnd($this->dbHandler->quoteColumn('language_mask'), -2)
422
            )
423
            ->where(
424
                $q->expr->eq(
425
                    $this->dbHandler->quoteColumn('id'),
426
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
427
                )
428
            );
429
        $q->prepare()->execute();
430
431
        // Now we need to update ezcontentobject_name
432
        /** @var $qName \eZ\Publish\Core\Persistence\Database\UpdateQuery */
433
        $qName = $this->dbHandler->createUpdateQuery();
434
        $qName
435
            ->update($this->dbHandler->quoteTable('ezcontentobject_name'))
436
            ->set(
437
                $this->dbHandler->quoteColumn('language_id'),
438
                $alwaysAvailable ?
439
                    $qName->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
440
                    $qName->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
441
            )
442
            ->where(
443
                $qName->expr->lAnd(
444
                    $qName->expr->eq(
445
                        $this->dbHandler->quoteColumn('contentobject_id'),
446
                        $qName->bindValue($contentId, null, \PDO::PARAM_INT)
447
                    ),
448
                    $qName->expr->eq(
449
                        $this->dbHandler->quoteColumn('content_version'),
450
                        $qName->bindValue(
451
                            $contentInfoRow['current_version'],
452
                            null,
453
                            \PDO::PARAM_INT
454
                        )
455
                    )
456
                )
457
            );
458
        $qName->prepare()->execute();
459
460
        // Now update ezcontentobject_attribute for current version
461
        // Create update query that will be reused
462
        /** @var $qAttr \eZ\Publish\Core\Persistence\Database\UpdateQuery */
463
        $qAttr = $this->dbHandler->createUpdateQuery();
464
        $qAttr
465
            ->update($this->dbHandler->quoteTable('ezcontentobject_attribute'))
466
            ->where(
467
                $qAttr->expr->lAnd(
468
                    $qAttr->expr->eq(
469
                        $this->dbHandler->quoteColumn('contentobject_id'),
470
                        $qAttr->bindValue($contentId, null, \PDO::PARAM_INT)
471
                    ),
472
                    $qAttr->expr->eq(
473
                        $this->dbHandler->quoteColumn('version'),
474
                        $qAttr->bindValue(
475
                            $contentInfoRow['current_version'],
476
                            null,
477
                            \PDO::PARAM_INT
478
                        )
479
                    )
480
                )
481
            );
482
483
        // If there is only a single language, update all fields and return
484
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($contentInfoRow['language_mask'])) {
485
            $qAttr->set(
486
                $this->dbHandler->quoteColumn('language_id'),
487
                $alwaysAvailable ?
488
                    $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
489
                    $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
490
            );
491
            $qAttr->prepare()->execute();
492
493
            return;
494
        }
495
496
        // Otherwise:
497
        // 1. Remove always available flag on all fields
498
        $qAttr->set(
499
            $this->dbHandler->quoteColumn('language_id'),
500
            $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
501
        );
502
        $qAttr->prepare()->execute();
503
504
        // 2. If Content is always available set the flag only on fields in main language
505
        if ($alwaysAvailable) {
506
            $qAttr->set(
507
                $this->dbHandler->quoteColumn('language_id'),
508
                $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1)
509
            );
510
            $qAttr->where(
511
                $qAttr->expr->gt(
512
                    $qAttr->expr->bitAnd(
513
                        $this->dbHandler->quoteColumn('language_id'),
514
                        $qAttr->bindValue($contentInfoRow['initial_language_id'], null, PDO::PARAM_INT)
515
                    ),
516
                    $qAttr->bindValue(0, null, PDO::PARAM_INT)
517
                )
518
            );
519
            $qAttr->prepare()->execute();
520
        }
521
    }
522
523
    /**
524
     * Sets the status of the version identified by $contentId and $version to $status.
525
     *
526
     * The $status can be one of STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED
527
     *
528
     * @param int $contentId
529
     * @param int $version
530
     * @param int $status
531
     *
532
     * @return bool
533
     */
534
    public function setStatus($contentId, $version, $status)
535
    {
536
        $q = $this->dbHandler->createUpdateQuery();
537
        $q->update(
538
            $this->dbHandler->quoteTable('ezcontentobject_version')
539
        )->set(
540
            $this->dbHandler->quoteColumn('status'),
541
            $q->bindValue($status, null, \PDO::PARAM_INT)
542
        )->set(
543
            $this->dbHandler->quoteColumn('modified'),
544
            $q->bindValue(time(), null, \PDO::PARAM_INT)
545
        )->where(
546
            $q->expr->lAnd(
547
                $q->expr->eq(
548
                    $this->dbHandler->quoteColumn('contentobject_id'),
549
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
550
                ),
551
                $q->expr->eq(
552
                    $this->dbHandler->quoteColumn('version'),
553
                    $q->bindValue($version, null, \PDO::PARAM_INT)
554
                )
555
            )
556
        );
557
        $statement = $q->prepare();
558
        $statement->execute();
559
560
        if ((bool)$statement->rowCount() === false) {
561
            return false;
562
        }
563
564
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
565
            return true;
566
        }
567
568
        // If the version's status is PUBLISHED, we set the content to published status as well
569
        $q = $this->dbHandler->createUpdateQuery();
570
        $q->update(
571
            $this->dbHandler->quoteTable('ezcontentobject')
572
        )->set(
573
            $this->dbHandler->quoteColumn('status'),
574
            $q->bindValue(ContentInfo::STATUS_PUBLISHED, null, \PDO::PARAM_INT)
575
        )->set(
576
            $this->dbHandler->quoteColumn('current_version'),
577
            $q->bindValue($version, null, \PDO::PARAM_INT)
578
        )->where(
579
            $q->expr->eq(
580
                $this->dbHandler->quoteColumn('id'),
581
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
582
            )
583
        );
584
        $statement = $q->prepare();
585
        $statement->execute();
586
587
        return (bool)$statement->rowCount();
588
    }
589
590
    public function setPublishedStatus(int $contentId, int $versionNo): void
591
    {
592
        $query = $this->getSetVersionStatusQuery(
593
            $contentId,
594
            $versionNo,
595
            VersionInfo::STATUS_PUBLISHED
596
        );
597
598
        /* this part allows set status `published` only if there is no other published version of the content */
599
        $notExistPublishedVersion = <<< HEREDOC
600
            NOT EXISTS (
601
                SELECT 1 FROM (
602
                    SELECT 1 FROM ezcontentobject_version  WHERE contentobject_id = :contentId AND status = :status 
603
                ) as V
604
            )
605
HEREDOC;
606
607
        $query->andWhere($notExistPublishedVersion);
608
        if (0 === $query->execute()) {
609
            throw new BadStateException(
610
                '$contentId', "Someone just published another Version of the Content item {$contentId}"
611
            );
612
        }
613
        $this->markContentAsPublished($contentId, $versionNo);
614
    }
615
616
    private function getSetVersionStatusQuery(
617
        int $contentId,
618
        int $versionNo,
619
        int $versionStatus
620
    ): DoctrineQueryBuilder {
621
        $query = $this->connection->createQueryBuilder();
622
        $query
623
            ->update('ezcontentobject_version')
624
            ->set('status', ':status')
625
            ->set('modified', ':modified')
626
            ->where('contentobject_id = :contentId')
627
            ->andWhere('version = :versionNo')
628
            ->setParameter('status', $versionStatus, ParameterType::INTEGER)
629
            ->setParameter('modified', time(), ParameterType::INTEGER)
630
            ->setParameter('contentId', $contentId, ParameterType::INTEGER)
631
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER);
632
633
        return $query;
634
    }
635
636
    private function markContentAsPublished(int $contentId, int $versionNo): void
637
    {
638
        $query = $this->connection->createQueryBuilder();
639
        $query
640
            ->update('ezcontentobject')
641
            ->set('status', ':status')
642
            ->set('current_version', ':versionNo')
643
            ->where('id =:contentId')
644
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
645
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER)
646
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
647
        $query->execute();
648
    }
649
650
    /**
651
     * Inserts a new field.
652
     *
653
     * Only used when a new field is created (i.e. a new object or a field in a
654
     * new language!). After that, field IDs need to stay the same, only the
655
     * version number changes.
656
     *
657
     * @param \eZ\Publish\SPI\Persistence\Content $content
658
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
659
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
660
     *
661
     * @return int ID
662
     */
663
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value)
664
    {
665
        $q = $this->dbHandler->createInsertQuery();
666
667
        $this->setInsertFieldValues($q, $content, $field, $value);
668
669
        // Insert with auto increment ID
670
        $q->set(
671
            $this->dbHandler->quoteColumn('id'),
672
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_attribute', 'id')
673
        );
674
675
        $q->prepare()->execute();
676
677
        return (int)$this->dbHandler->lastInsertId(
678
            $this->dbHandler->getSequenceName('ezcontentobject_attribute', 'id')
679
        );
680
    }
681
682
    /**
683
     * Inserts an existing field.
684
     *
685
     * Used to insert a field with an exsting ID but a new version number.
686
     *
687
     * @param Content $content
688
     * @param Field $field
689
     * @param StorageFieldValue $value
690
     */
691
    public function insertExistingField(Content $content, Field $field, StorageFieldValue $value)
692
    {
693
        $q = $this->dbHandler->createInsertQuery();
694
695
        $this->setInsertFieldValues($q, $content, $field, $value);
696
697
        $q->set(
698
            $this->dbHandler->quoteColumn('id'),
699
            $q->bindValue($field->id, null, \PDO::PARAM_INT)
700
        );
701
702
        $q->prepare()->execute();
703
    }
704
705
    /**
706
     * Sets field (ezcontentobject_attribute) values to the given query.
707
     *
708
     * @param \eZ\Publish\Core\Persistence\Database\InsertQuery $q
709
     * @param Content $content
710
     * @param Field $field
711
     * @param StorageFieldValue $value
712
     */
713
    protected function setInsertFieldValues(InsertQuery $q, Content $content, Field $field, StorageFieldValue $value)
714
    {
715
        $q->insertInto(
716
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
717
        )->set(
718
            $this->dbHandler->quoteColumn('contentobject_id'),
719
            $q->bindValue($content->versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
720
        )->set(
721
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
722
            $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
723
        )->set(
724
            $this->dbHandler->quoteColumn('data_type_string'),
725
            $q->bindValue($field->type)
726
        )->set(
727
            $this->dbHandler->quoteColumn('language_code'),
728
            $q->bindValue($field->languageCode)
729
        )->set(
730
            $this->dbHandler->quoteColumn('version'),
731
            $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
732
        )->set(
733
            $this->dbHandler->quoteColumn('data_float'),
734
            $q->bindValue($value->dataFloat)
735
        )->set(
736
            $this->dbHandler->quoteColumn('data_int'),
737
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
738
        )->set(
739
            $this->dbHandler->quoteColumn('data_text'),
740
            $q->bindValue($value->dataText)
741
        )->set(
742
            $this->dbHandler->quoteColumn('sort_key_int'),
743
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
744
        )->set(
745
            $this->dbHandler->quoteColumn('sort_key_string'),
746
            $q->bindValue(mb_substr((string)$value->sortKeyString, 0, 255))
747
        )->set(
748
            $this->dbHandler->quoteColumn('language_id'),
749
            $q->bindValue(
750
                $this->languageMaskGenerator->generateLanguageIndicator(
751
                    $field->languageCode,
752
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
753
                ),
754
                null,
755
                \PDO::PARAM_INT
756
            )
757
        );
758
    }
759
760
    /**
761
     * Checks if $languageCode is always available in $content.
762
     *
763
     * @param \eZ\Publish\SPI\Persistence\Content $content
764
     * @param string $languageCode
765
     *
766
     * @return bool
767
     */
768
    protected function isLanguageAlwaysAvailable(Content $content, $languageCode)
769
    {
770
        return
771
            $content->versionInfo->contentInfo->alwaysAvailable &&
772
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
773
        ;
774
    }
775
776
    /**
777
     * Updates an existing field.
778
     *
779
     * @param Field $field
780
     * @param StorageFieldValue $value
781
     */
782
    public function updateField(Field $field, StorageFieldValue $value)
783
    {
784
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
785
        // cannot change on update
786
        $q = $this->dbHandler->createUpdateQuery();
787
        $this->setFieldUpdateValues($q, $value);
788
        $q->where(
789
            $q->expr->lAnd(
790
                $q->expr->eq(
791
                    $this->dbHandler->quoteColumn('id'),
792
                    $q->bindValue($field->id, null, \PDO::PARAM_INT)
793
                ),
794
                $q->expr->eq(
795
                    $this->dbHandler->quoteColumn('version'),
796
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
797
                )
798
            )
799
        );
800
        $q->prepare()->execute();
801
    }
802
803
    /**
804
     * Sets update fields for $value on $q.
805
     *
806
     * @param \eZ\Publish\Core\Persistence\Database\UpdateQuery $q
807
     * @param StorageFieldValue $value
808
     */
809
    protected function setFieldUpdateValues(UpdateQuery $q, StorageFieldValue $value)
810
    {
811
        $q->update(
812
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
813
        )->set(
814
            $this->dbHandler->quoteColumn('data_float'),
815
            $q->bindValue($value->dataFloat)
816
        )->set(
817
            $this->dbHandler->quoteColumn('data_int'),
818
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
819
        )->set(
820
            $this->dbHandler->quoteColumn('data_text'),
821
            $q->bindValue($value->dataText)
822
        )->set(
823
            $this->dbHandler->quoteColumn('sort_key_int'),
824
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
825
        )->set(
826
            $this->dbHandler->quoteColumn('sort_key_string'),
827
            $q->bindValue(mb_substr((string)$value->sortKeyString, 0, 255))
828
        );
829
    }
830
831
    /**
832
     * Updates an existing, non-translatable field.
833
     *
834
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
835
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
836
     * @param int $contentId
837
     */
838
    public function updateNonTranslatableField(
839
        Field $field,
840
        StorageFieldValue $value,
841
        $contentId
842
    ) {
843
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
844
        // cannot change on update
845
        $q = $this->dbHandler->createUpdateQuery();
846
        $this->setFieldUpdateValues($q, $value);
847
        $q->where(
848
            $q->expr->lAnd(
849
                $q->expr->eq(
850
                    $this->dbHandler->quoteColumn('contentclassattribute_id'),
851
                    $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
852
                ),
853
                $q->expr->eq(
854
                    $this->dbHandler->quoteColumn('contentobject_id'),
855
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
856
                ),
857
                $q->expr->eq(
858
                    $this->dbHandler->quoteColumn('version'),
859
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
860
                )
861
            )
862
        );
863
        $q->prepare()->execute();
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869
    public function load($contentId, $version = null, array $translations = null)
870
    {
871
        return $this->internalLoadContent([$contentId], $version, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 869 can also be of type array; however, eZ\Publish\Core\Persiste...::internalLoadContent() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
872
    }
873
874
    /**
875
     * {@inheritdoc}
876
     */
877
    public function loadContentList(array $contentIds, array $translations = null): array
878
    {
879
        return $this->internalLoadContent($contentIds, null, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 877 can also be of type array; however, eZ\Publish\Core\Persiste...::internalLoadContent() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
880
    }
881
882
    /**
883
     * @see load(), loadContentList()
884
     *
885
     * @param array $contentIds
886
     * @param int|null $version
887
     * @param string[]|null $translations
888
     *
889
     * @return array
890
     */
891
    private function internalLoadContent(array $contentIds, int $version = null, array $translations = null): array
892
    {
893
        $queryBuilder = $this->connection->createQueryBuilder();
894
        $expr = $queryBuilder->expr();
895
        $queryBuilder
896
            ->select(
897
                'c.id AS ezcontentobject_id',
898
                'c.contentclass_id AS ezcontentobject_contentclass_id',
899
                'c.section_id AS ezcontentobject_section_id',
900
                'c.owner_id AS ezcontentobject_owner_id',
901
                'c.remote_id AS ezcontentobject_remote_id',
902
                'c.current_version AS ezcontentobject_current_version',
903
                'c.initial_language_id AS ezcontentobject_initial_language_id',
904
                'c.modified AS ezcontentobject_modified',
905
                'c.published AS ezcontentobject_published',
906
                'c.status AS ezcontentobject_status',
907
                'c.name AS ezcontentobject_name',
908
                'c.language_mask AS ezcontentobject_language_mask',
909
                'c.is_hidden AS ezcontentobject_is_hidden',
910
                'v.id AS ezcontentobject_version_id',
911
                'v.version AS ezcontentobject_version_version',
912
                'v.modified AS ezcontentobject_version_modified',
913
                'v.creator_id AS ezcontentobject_version_creator_id',
914
                'v.created AS ezcontentobject_version_created',
915
                'v.status AS ezcontentobject_version_status',
916
                'v.language_mask AS ezcontentobject_version_language_mask',
917
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
918
                'a.id AS ezcontentobject_attribute_id',
919
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
920
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
921
                'a.language_code AS ezcontentobject_attribute_language_code',
922
                'a.language_id AS ezcontentobject_attribute_language_id',
923
                'a.data_float AS ezcontentobject_attribute_data_float',
924
                'a.data_int AS ezcontentobject_attribute_data_int',
925
                'a.data_text AS ezcontentobject_attribute_data_text',
926
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
927
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
928
                't.main_node_id AS ezcontentobject_tree_main_node_id'
929
            )
930
            ->from('ezcontentobject', 'c')
931
            ->innerJoin(
932
                'c',
933
                'ezcontentobject_version',
934
                'v',
935
                $expr->andX(
936
                    $expr->eq('c.id', 'v.contentobject_id'),
937
                    $expr->eq('v.version', $version ?? 'c.current_version')
938
                )
939
            )
940
            ->innerJoin(
941
                'v',
942
                'ezcontentobject_attribute',
943
                'a',
944
                $expr->andX(
945
                    $expr->eq('v.contentobject_id', 'a.contentobject_id'),
946
                    $expr->eq('v.version', 'a.version')
947
                )
948
            )
949
            ->leftJoin(
950
                'c',
951
                'ezcontentobject_tree',
952
                't',
953
                $expr->andX(
954
                    $expr->eq('c.id', 't.contentobject_id'),
955
                    $expr->eq('t.node_id', 't.main_node_id')
956
                )
957
            );
958
959
        $queryBuilder->where(
960
            $expr->in(
961
                'c.id',
962
                $queryBuilder->createNamedParameter($contentIds, Connection::PARAM_INT_ARRAY)
963
            )
964
        );
965
966
        if (!empty($translations)) {
967
            $queryBuilder->andWhere(
968
                $expr->in(
969
                    'a.language_code',
970
                    $queryBuilder->createNamedParameter($translations, Connection::PARAM_STR_ARRAY)
971
                )
972
            );
973
        }
974
975
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
976
    }
977
978
    /**
979
     * Get query builder to load Content Info data.
980
     *
981
     * @see loadContentInfo(), loadContentInfoByRemoteId(), loadContentInfoList(), loadContentInfoByLocationId()
982
     *
983
     * @param bool $joinMainLocation
984
     *
985
     * @return \Doctrine\DBAL\Query\QueryBuilder
986
     */
987
    private function createLoadContentInfoQueryBuilder(bool $joinMainLocation = true): DoctrineQueryBuilder
988
    {
989
        $queryBuilder = $this->connection->createQueryBuilder();
990
        $expr = $queryBuilder->expr();
991
992
        $joinCondition = $expr->eq('c.id', 't.contentobject_id');
993
        if ($joinMainLocation) {
994
            // wrap join condition with AND operator and join by a Main Location
995
            $joinCondition = $expr->andX(
996
                $joinCondition,
997
                $expr->eq('t.node_id', 't.main_node_id')
998
            );
999
        }
1000
1001
        $queryBuilder
1002
            ->select('c.*', 't.main_node_id AS ezcontentobject_tree_main_node_id')
1003
            ->from('ezcontentobject', 'c')
1004
            ->leftJoin(
1005
                'c',
1006
                'ezcontentobject_tree',
1007
                't',
1008
                $joinCondition
0 ignored issues
show
Bug introduced by
It seems like $joinCondition defined by $expr->andX($joinConditi...id', 't.main_node_id')) on line 995 can also be of type object<Doctrine\DBAL\Que...on\CompositeExpression>; however, Doctrine\DBAL\Query\QueryBuilder::leftJoin() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1009
            );
1010
1011
        return $queryBuilder;
1012
    }
1013
1014
    /**
1015
     * Loads info for content identified by $contentId.
1016
     * Will basically return a hash containing all field values for ezcontentobject table plus some additional keys:
1017
     *  - always_available => Boolean indicating if content's language mask contains alwaysAvailable bit field
1018
     *  - main_language_code => Language code for main (initial) language. E.g. "eng-GB".
1019
     *
1020
     * @param int $contentId
1021
     *
1022
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1023
     *
1024
     * @return array
1025
     */
1026
    public function loadContentInfo($contentId)
1027
    {
1028
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1029
        $queryBuilder
1030
            ->where('c.id = :id')
1031
            ->setParameter('id', $contentId, ParameterType::INTEGER);
1032
1033
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1034
        if (empty($results)) {
1035
            throw new NotFound('content', "id: $contentId");
1036
        }
1037
1038
        return $results[0];
1039
    }
1040
1041
    public function loadContentInfoList(array $contentIds)
1042
    {
1043
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1044
        $queryBuilder
1045
            ->where('c.id IN (:ids)')
1046
            ->setParameter('ids', $contentIds, Connection::PARAM_INT_ARRAY);
1047
1048
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1049
    }
1050
1051
    /**
1052
     * Loads info for a content object identified by its remote ID.
1053
     *
1054
     * Returns an array with the relevant data.
1055
     *
1056
     * @param mixed $remoteId
1057
     *
1058
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1059
     *
1060
     * @return array
1061
     */
1062
    public function loadContentInfoByRemoteId($remoteId)
1063
    {
1064
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1065
        $queryBuilder
1066
            ->where('c.remote_id = :id')
1067
            ->setParameter('id', $remoteId, ParameterType::STRING);
1068
1069
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1070
        if (empty($results)) {
1071
            throw new NotFound('content', "remote_id: $remoteId");
1072
        }
1073
1074
        return $results[0];
1075
    }
1076
1077
    /**
1078
     * Loads info for a content object identified by its location ID (node ID).
1079
     *
1080
     * Returns an array with the relevant data.
1081
     *
1082
     * @param int $locationId
1083
     *
1084
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1085
     *
1086
     * @return array
1087
     */
1088
    public function loadContentInfoByLocationId($locationId)
1089
    {
1090
        $queryBuilder = $this->createLoadContentInfoQueryBuilder(false);
1091
        $queryBuilder
1092
            ->where('t.node_id = :id')
1093
            ->setParameter('id', $locationId, ParameterType::INTEGER);
1094
1095
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1096
        if (empty($results)) {
1097
            throw new NotFound('content', "node_id: $locationId");
1098
        }
1099
1100
        return $results[0];
1101
    }
1102
1103
    /**
1104
     * Loads version info for content identified by $contentId and $versionNo.
1105
     * Will basically return a hash containing all field values from ezcontentobject_version table plus following keys:
1106
     *  - names => Hash of content object names. Key is the language code, value is the name.
1107
     *  - languages => Hash of language ids. Key is the language code (e.g. "eng-GB"), value is the language numeric id without the always available bit.
1108
     *  - initial_language_code => Language code for initial language in this version.
1109
     *
1110
     * @param int $contentId
1111
     * @param int|null $versionNo
1112
     *
1113
     * @return array
1114
     */
1115
    public function loadVersionInfo($contentId, $versionNo = null)
1116
    {
1117
        $queryBuilder = $this->queryBuilder->createVersionInfoQueryBuilder($versionNo);
1118
        $queryBuilder->where(
1119
            $queryBuilder->expr()->eq(
1120
                'c.id',
1121
                $queryBuilder->createNamedParameter($contentId, PDO::PARAM_INT)
1122
            )
1123
        );
1124
1125
        return $queryBuilder->execute()->fetchAll(PDO::FETCH_ASSOC);
1126
    }
1127
1128
    /**
1129
     * Returns the number of all versions with given status created by the given $userId for content which is not in Trash.
1130
     *
1131
     * @param int $userId
1132
     * @param int $status
1133
     *
1134
     * @return int
1135
     */
1136
    public function countVersionsForUser(int $userId, int $status = VersionInfo::STATUS_DRAFT): int
1137
    {
1138
        $platform = $this->connection->getDatabasePlatform();
1139
        $query = $this->connection->createQueryBuilder();
1140
        $expr = $query->expr();
1141
        $query
1142
            ->select($platform->getCountExpression('v.id'))
1143
            ->from('ezcontentobject_version', 'v')
1144
            ->innerJoin(
1145
                'v',
1146
                'ezcontentobject',
1147
                'c',
1148
                $expr->andX(
1149
                    $expr->eq('c.id', 'v.contentobject_id'),
1150
                    $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
1151
                )
1152
            )
1153
            ->where(
1154
                $query->expr()->andX(
1155
                    $query->expr()->eq('v.status', ':status'),
1156
                    $query->expr()->eq('v.creator_id', ':user_id')
1157
                )
1158
            )
1159
            ->setParameter(':status', $status, \PDO::PARAM_INT)
1160
            ->setParameter(':user_id', $userId, \PDO::PARAM_INT);
1161
1162
        return (int) $query->execute()->fetchColumn();
1163
    }
1164
1165
    /**
1166
     * Returns data for all versions with given status created by the given $userId.
1167
     *
1168
     * @param int $userId
1169
     * @param int $status
1170
     *
1171
     * @return string[][]
1172
     */
1173
    public function listVersionsForUser($userId, $status = VersionInfo::STATUS_DRAFT)
1174
    {
1175
        $query = $this->queryBuilder->createVersionInfoFindQuery();
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Persiste...eVersionInfoFindQuery() has been deprecated with message: Move to Doctrine based query builder {@see createVersionInfoQueryBuilder}.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1176
        $query->where(
1177
            $query->expr->lAnd(
1178
                $query->expr->eq(
1179
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1180
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1181
                ),
1182
                $query->expr->eq(
1183
                    $this->dbHandler->quoteColumn('creator_id', 'ezcontentobject_version'),
1184
                    $query->bindValue($userId, null, \PDO::PARAM_INT)
1185
                )
1186
            )
1187
        );
1188
1189
        return $this->listVersionsHelper($query);
1190
    }
1191
1192
    /**
1193
     * {@inheritdoc}
1194
     */
1195
    public function loadVersionsForUser($userId, $status = VersionInfo::STATUS_DRAFT, int $offset = 0, int $limit = -1): array
1196
    {
1197
        $query = $this->createVersionInfoFindQueryBuilder();
1198
        $expr = $query->expr();
1199
        $query->where(
1200
            $expr->andX(
1201
                $expr->eq('v.status', ':status'),
1202
                $expr->eq('v.creator_id', ':user_id'),
1203
                $expr->neq('c.status', ContentInfo::STATUS_TRASHED)
1204
            )
1205
        )
1206
        ->setFirstResult($offset)
1207
        ->setParameter(':status', $status, \PDO::PARAM_INT)
1208
        ->setParameter(':user_id', $userId, \PDO::PARAM_INT);
1209
1210
        if ($limit > 0) {
1211
            $query->setMaxResults($limit);
1212
        }
1213
1214
        $query->orderBy('v.modified', 'DESC');
1215
        $query->addOrderBy('v.id', 'DESC');
1216
1217
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1218
    }
1219
1220
    /**
1221
     * Returns all version data for the given $contentId, optionally filtered by status.
1222
     *
1223
     * Result is returned with oldest version first (using version id as it has index and is auto increment).
1224
     *
1225
     * @param mixed $contentId
1226
     * @param mixed|null $status Optional argument to filter versions by status, like {@see VersionInfo::STATUS_ARCHIVED}.
1227
     * @param int $limit Limit for items returned, -1 means none.
1228
     *
1229
     * @return string[][]
1230
     */
1231
    public function listVersions($contentId, $status = null, $limit = -1)
1232
    {
1233
        $query = $this->queryBuilder->createVersionInfoFindQuery();
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Persiste...eVersionInfoFindQuery() has been deprecated with message: Move to Doctrine based query builder {@see createVersionInfoQueryBuilder}.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1234
1235
        $filter = $query->expr->eq(
1236
            $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1237
            $query->bindValue($contentId, null, \PDO::PARAM_INT)
1238
        );
1239
1240
        if ($status !== null) {
1241
            $filter = $query->expr->lAnd(
1242
                $filter,
1243
                $query->expr->eq(
1244
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1245
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1246
                )
1247
            );
1248
        }
1249
1250
        $query->where($filter);
1251
1252
        if ($limit > 0) {
1253
            $query->limit($limit);
1254
        }
1255
1256
        return $this->listVersionsHelper($query);
1257
    }
1258
1259
    /**
1260
     * Helper for {@see listVersions()} and {@see listVersionsForUser()} that filters duplicates
1261
     * that are the result of the cartesian product performed by createVersionInfoFindQuery().
1262
     *
1263
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
1264
     *
1265
     * @return string[][]
1266
     */
1267
    private function listVersionsHelper(SelectQuery $query)
1268
    {
1269
        $query->orderBy(
1270
            $this->dbHandler->quoteColumn('id', 'ezcontentobject_version')
1271
        );
1272
1273
        $statement = $query->prepare();
1274
        $statement->execute();
1275
1276
        $results = [];
1277
        $previousId = null;
1278
        foreach ($statement->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1279
            if ($row['ezcontentobject_version_id'] == $previousId) {
1280
                continue;
1281
            }
1282
1283
            $previousId = $row['ezcontentobject_version_id'];
1284
            $results[] = $row;
1285
        }
1286
1287
        return $results;
1288
    }
1289
1290
    /**
1291
     * Returns all version numbers for the given $contentId.
1292
     *
1293
     * @param mixed $contentId
1294
     *
1295
     * @return int[]
1296
     */
1297
    public function listVersionNumbers($contentId)
1298
    {
1299
        $query = $this->dbHandler->createSelectQuery();
1300
        $query->selectDistinct(
1301
            $this->dbHandler->quoteColumn('version')
1302
        )->from(
1303
            $this->dbHandler->quoteTable('ezcontentobject_version')
1304
        )->where(
1305
            $query->expr->eq(
1306
                $this->dbHandler->quoteColumn('contentobject_id'),
1307
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1308
            )
1309
        );
1310
1311
        $statement = $query->prepare();
1312
        $statement->execute();
1313
1314
        return array_map('intval', $statement->fetchAll(\PDO::FETCH_COLUMN));
1315
    }
1316
1317
    /**
1318
     * Returns last version number for content identified by $contentId.
1319
     *
1320
     * @param int $contentId
1321
     *
1322
     * @return int
1323
     */
1324
    public function getLastVersionNumber($contentId)
1325
    {
1326
        $query = $this->dbHandler->createSelectQuery();
1327
        $query->select(
1328
            $query->expr->max($this->dbHandler->quoteColumn('version'))
1329
        )->from(
1330
            $this->dbHandler->quoteTable('ezcontentobject_version')
1331
        )->where(
1332
            $query->expr->eq(
1333
                $this->dbHandler->quoteColumn('contentobject_id'),
1334
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1335
            )
1336
        );
1337
1338
        $statement = $query->prepare();
1339
        $statement->execute();
1340
1341
        return (int)$statement->fetchColumn();
1342
    }
1343
1344
    /**
1345
     * Returns all IDs for locations that refer to $contentId.
1346
     *
1347
     * @param int $contentId
1348
     *
1349
     * @return int[]
1350
     */
1351
    public function getAllLocationIds($contentId)
1352
    {
1353
        $query = $this->dbHandler->createSelectQuery();
1354
        $query->select(
1355
            $this->dbHandler->quoteColumn('node_id')
1356
        )->from(
1357
            $this->dbHandler->quoteTable('ezcontentobject_tree')
1358
        )->where(
1359
            $query->expr->eq(
1360
                $this->dbHandler->quoteColumn('contentobject_id'),
1361
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1362
            )
1363
        );
1364
1365
        $statement = $query->prepare();
1366
        $statement->execute();
1367
1368
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1369
    }
1370
1371
    /**
1372
     * Returns all field IDs of $contentId grouped by their type.
1373
     * If $versionNo is set only field IDs for that version are returned.
1374
     * If $languageCode is set, only field IDs for that language are returned.
1375
     *
1376
     * @param int $contentId
1377
     * @param int|null $versionNo
1378
     * @param string|null $languageCode
1379
     *
1380
     * @return int[][]
1381
     */
1382
    public function getFieldIdsByType($contentId, $versionNo = null, $languageCode = null)
1383
    {
1384
        $query = $this->dbHandler->createSelectQuery();
1385
        $query->select(
1386
            $this->dbHandler->quoteColumn('id'),
1387
            $this->dbHandler->quoteColumn('data_type_string')
1388
        )->from(
1389
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1390
        )->where(
1391
            $query->expr->eq(
1392
                $this->dbHandler->quoteColumn('contentobject_id'),
1393
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1394
            )
1395
        );
1396
1397
        if (isset($versionNo)) {
1398
            $query->where(
1399
                $query->expr->eq(
1400
                    $this->dbHandler->quoteColumn('version'),
1401
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1402
                )
1403
            );
1404
        }
1405
1406
        if (isset($languageCode)) {
1407
            $query->where(
1408
                $query->expr->eq(
1409
                    $this->dbHandler->quoteColumn('language_code'),
1410
                    $query->bindValue($languageCode, null, \PDO::PARAM_STR)
1411
                )
1412
            );
1413
        }
1414
1415
        $statement = $query->prepare();
1416
        $statement->execute();
1417
1418
        $result = [];
1419
        foreach ($statement->fetchAll() as $row) {
1420
            if (!isset($result[$row['data_type_string']])) {
1421
                $result[$row['data_type_string']] = [];
1422
            }
1423
            $result[$row['data_type_string']][] = (int)$row['id'];
1424
        }
1425
1426
        return $result;
1427
    }
1428
1429
    /**
1430
     * Deletes relations to and from $contentId.
1431
     * If $versionNo is set only relations for that version are deleted.
1432
     *
1433
     * @param int $contentId
1434
     * @param int|null $versionNo
1435
     */
1436
    public function deleteRelations($contentId, $versionNo = null)
1437
    {
1438
        $query = $this->dbHandler->createDeleteQuery();
1439
        $query->deleteFrom(
1440
            $this->dbHandler->quoteTable('ezcontentobject_link')
1441
        );
1442
1443
        if (isset($versionNo)) {
1444
            $query->where(
1445
                $query->expr->lAnd(
1446
                    $query->expr->eq(
1447
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1448
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1449
                    ),
1450
                    $query->expr->eq(
1451
                        $this->dbHandler->quoteColumn('from_contentobject_version'),
1452
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1453
                    )
1454
                )
1455
            );
1456
        } else {
1457
            $query->where(
1458
                $query->expr->lOr(
1459
                    $query->expr->eq(
1460
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1461
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1462
                    ),
1463
                    $query->expr->eq(
1464
                        $this->dbHandler->quoteColumn('to_contentobject_id'),
1465
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1466
                    )
1467
                )
1468
            );
1469
        }
1470
1471
        $query->prepare()->execute();
1472
    }
1473
1474
    /**
1475
     * Removes relations to Content with $contentId from Relation and RelationList field type fields.
1476
     *
1477
     * @param int $contentId
1478
     */
1479
    public function removeReverseFieldRelations($contentId)
1480
    {
1481
        $query = $this->dbHandler->createSelectQuery();
1482
        $query
1483
            ->select('ezcontentobject_attribute.*')
1484
            ->from('ezcontentobject_attribute')
1485
            ->innerJoin(
1486
                'ezcontentobject_link',
1487
                $query->expr->lAnd(
1488
                    $query->expr->eq(
1489
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1490
                        $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_attribute')
1491
                    ),
1492
                    $query->expr->eq(
1493
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1494
                        $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute')
1495
                    ),
1496
                    $query->expr->eq(
1497
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_link'),
1498
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_attribute')
1499
                    )
1500
                )
1501
            )
1502
            ->where(
1503
                $query->expr->eq(
1504
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1505
                    $query->bindValue($contentId, null, PDO::PARAM_INT)
1506
                ),
1507
                $query->expr->gt(
1508
                    $query->expr->bitAnd(
1509
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1510
                        $query->bindValue(8, null, PDO::PARAM_INT)
1511
                    ),
1512
                    0
1513
                )
1514
            );
1515
1516
        $statement = $query->prepare();
1517
        $statement->execute();
1518
1519
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
1520
            if ($row['data_type_string'] === 'ezobjectrelation') {
1521
                $this->removeRelationFromRelationField($row);
1522
            }
1523
1524
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1525
                $this->removeRelationFromRelationListField($contentId, $row);
1526
            }
1527
        }
1528
    }
1529
1530
    /**
1531
     * Updates field value of RelationList field type identified by given $row data,
1532
     * removing relations toward given $contentId.
1533
     *
1534
     * @param int $contentId
1535
     * @param array $row
1536
     */
1537
    protected function removeRelationFromRelationListField($contentId, array $row)
1538
    {
1539
        $document = new DOMDocument('1.0', 'utf-8');
1540
        $document->loadXML($row['data_text']);
1541
1542
        $xpath = new DOMXPath($document);
1543
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1544
1545
        $relationItems = $xpath->query($xpathExpression);
1546
        foreach ($relationItems as $relationItem) {
1547
            $relationItem->parentNode->removeChild($relationItem);
1548
        }
1549
1550
        $query = $this->dbHandler->createUpdateQuery();
1551
        $query
1552
            ->update('ezcontentobject_attribute')
1553
            ->set(
1554
                'data_text',
1555
                $query->bindValue($document->saveXML(), null, PDO::PARAM_STR)
1556
            )
1557
            ->where(
1558
                $query->expr->lAnd(
1559
                    $query->expr->eq(
1560
                        $this->dbHandler->quoteColumn('id'),
1561
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1562
                    ),
1563
                    $query->expr->eq(
1564
                        $this->dbHandler->quoteColumn('version'),
1565
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1566
                    )
1567
                )
1568
            );
1569
1570
        $query->prepare()->execute();
1571
    }
1572
1573
    /**
1574
     * Updates field value of Relation field type identified by given $row data,
1575
     * removing relation data.
1576
     *
1577
     * @param array $row
1578
     */
1579
    protected function removeRelationFromRelationField(array $row)
1580
    {
1581
        $query = $this->dbHandler->createUpdateQuery();
1582
        $query
1583
            ->update('ezcontentobject_attribute')
1584
            ->set('data_int', $query->bindValue(null, null, PDO::PARAM_INT))
1585
            ->set('sort_key_int', $query->bindValue(0, null, PDO::PARAM_INT))
1586
            ->where(
1587
                $query->expr->lAnd(
1588
                    $query->expr->eq(
1589
                        $this->dbHandler->quoteColumn('id'),
1590
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1591
                    ),
1592
                    $query->expr->eq(
1593
                        $this->dbHandler->quoteColumn('version'),
1594
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1595
                    )
1596
                )
1597
            );
1598
1599
        $query->prepare()->execute();
1600
    }
1601
1602
    /**
1603
     * Deletes the field with the given $fieldId.
1604
     *
1605
     * @param int $fieldId
1606
     */
1607
    public function deleteField($fieldId)
1608
    {
1609
        $query = $this->dbHandler->createDeleteQuery();
1610
        $query->deleteFrom(
1611
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1612
        )->where(
1613
            $query->expr->eq(
1614
                $this->dbHandler->quoteColumn('id'),
1615
                $query->bindValue($fieldId, null, \PDO::PARAM_INT)
1616
            )
1617
        );
1618
1619
        $query->prepare()->execute();
1620
    }
1621
1622
    /**
1623
     * Deletes all fields of $contentId in all versions.
1624
     * If $versionNo is set only fields for that version are deleted.
1625
     *
1626
     * @param int $contentId
1627
     * @param int|null $versionNo
1628
     */
1629
    public function deleteFields($contentId, $versionNo = null)
1630
    {
1631
        $query = $this->dbHandler->createDeleteQuery();
1632
        $query->deleteFrom('ezcontentobject_attribute')
1633
            ->where(
1634
                $query->expr->eq(
1635
                    $this->dbHandler->quoteColumn('contentobject_id'),
1636
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1637
                )
1638
            );
1639
1640
        if (isset($versionNo)) {
1641
            $query->where(
1642
                $query->expr->eq(
1643
                    $this->dbHandler->quoteColumn('version'),
1644
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1645
                )
1646
            );
1647
        }
1648
1649
        $query->prepare()->execute();
1650
    }
1651
1652
    /**
1653
     * Deletes all versions of $contentId.
1654
     * If $versionNo is set only that version is deleted.
1655
     *
1656
     * @param int $contentId
1657
     * @param int|null $versionNo
1658
     */
1659
    public function deleteVersions($contentId, $versionNo = null)
1660
    {
1661
        $query = $this->dbHandler->createDeleteQuery();
1662
        $query->deleteFrom('ezcontentobject_version')
1663
            ->where(
1664
                $query->expr->eq(
1665
                    $this->dbHandler->quoteColumn('contentobject_id'),
1666
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1667
                )
1668
            );
1669
1670
        if (isset($versionNo)) {
1671
            $query->where(
1672
                $query->expr->eq(
1673
                    $this->dbHandler->quoteColumn('version'),
1674
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1675
                )
1676
            );
1677
        }
1678
1679
        $query->prepare()->execute();
1680
    }
1681
1682
    /**
1683
     * Deletes all names of $contentId.
1684
     * If $versionNo is set only names for that version are deleted.
1685
     *
1686
     * @param int $contentId
1687
     * @param int|null $versionNo
1688
     */
1689
    public function deleteNames($contentId, $versionNo = null)
1690
    {
1691
        $query = $this->dbHandler->createDeleteQuery();
1692
        $query->deleteFrom('ezcontentobject_name')
1693
            ->where(
1694
                $query->expr->eq(
1695
                    $this->dbHandler->quoteColumn('contentobject_id'),
1696
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1697
                )
1698
            );
1699
1700
        if (isset($versionNo)) {
1701
            $query->where(
1702
                $query->expr->eq(
1703
                    $this->dbHandler->quoteColumn('content_version'),
1704
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1705
                )
1706
            );
1707
        }
1708
1709
        $query->prepare()->execute();
1710
    }
1711
1712
    /**
1713
     * Sets the name for Content $contentId in version $version to $name in $language.
1714
     *
1715
     * @param int $contentId
1716
     * @param int $version
1717
     * @param string $name
1718
     * @param string $language
1719
     */
1720
    public function setName($contentId, $version, $name, $language)
1721
    {
1722
        $language = $this->languageHandler->loadByLanguageCode($language);
1723
1724
        // Is it an insert or an update ?
1725
        $qSelect = $this->dbHandler->createSelectQuery();
1726
        $qSelect
1727
            ->select(
1728
                $qSelect->alias($qSelect->expr->count('*'), 'count')
1729
            )
1730
            ->from($this->dbHandler->quoteTable('ezcontentobject_name'))
1731
            ->where(
1732
                $qSelect->expr->lAnd(
1733
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $qSelect->bindValue($contentId)),
1734
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_version'), $qSelect->bindValue($version)),
1735
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_translation'), $qSelect->bindValue($language->languageCode))
1736
                )
1737
            );
1738
        $stmt = $qSelect->prepare();
1739
        $stmt->execute();
1740
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1741
1742
        $insert = $res[0]['count'] == 0;
1743
        if ($insert) {
1744
            $q = $this->dbHandler->createInsertQuery();
1745
            $q->insertInto($this->dbHandler->quoteTable('ezcontentobject_name'));
1746
        } else {
1747
            $q = $this->dbHandler->createUpdateQuery();
1748
            $q->update($this->dbHandler->quoteTable('ezcontentobject_name'))
1749
                ->where(
1750
                    $q->expr->lAnd(
1751
                        $q->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $q->bindValue($contentId)),
1752
                        $q->expr->eq($this->dbHandler->quoteColumn('content_version'), $q->bindValue($version)),
1753
                        $q->expr->eq($this->dbHandler->quoteColumn('content_translation'), $q->bindValue($language->languageCode))
1754
                    )
1755
                );
1756
        }
1757
1758
        $q->set(
1759
            $this->dbHandler->quoteColumn('contentobject_id'),
1760
            $q->bindValue($contentId, null, \PDO::PARAM_INT)
1761
        )->set(
1762
            $this->dbHandler->quoteColumn('content_version'),
1763
            $q->bindValue($version, null, \PDO::PARAM_INT)
1764
        )->set(
1765
            $this->dbHandler->quoteColumn('language_id'),
1766
            '(' . $this->getLanguageQuery()->getQuery() . ')'
1767
        )->set(
1768
            $this->dbHandler->quoteColumn('content_translation'),
1769
            $q->bindValue($language->languageCode)
1770
        )->set(
1771
            $this->dbHandler->quoteColumn('real_translation'),
1772
            $q->bindValue($language->languageCode)
1773
        )->set(
1774
            $this->dbHandler->quoteColumn('name'),
1775
            $q->bindValue($name)
1776
        );
1777
        $q->bindValue($language->id, ':languageId', \PDO::PARAM_INT);
1778
        $q->bindValue($contentId, ':contentId', \PDO::PARAM_INT);
1779
        $q->prepare()->execute();
1780
    }
1781
1782
    /**
1783
     * Returns a language sub select query for setName.
1784
     *
1785
     * Return sub select query which gets proper language mask for alwaysAvailable Content.
1786
     *
1787
     * @return \eZ\Publish\Core\Persistence\Database\SelectQuery
1788
     */
1789
    private function getLanguageQuery()
1790
    {
1791
        $languageQuery = $this->dbHandler->createSelectQuery();
1792
        $languageQuery
1793
            ->select(
1794
                $languageQuery->expr->searchedCase(
1795
                    [
1796
                        $languageQuery->expr->lAnd(
1797
                            $languageQuery->expr->eq(
1798
                                $this->dbHandler->quoteColumn('initial_language_id'),
1799
                                ':languageId'
1800
                            ),
1801
                            // wrap bitwise check into another "neq" to provide cross-DBMS compatibility
1802
                            $languageQuery->expr->neq(
1803
                                $languageQuery->expr->bitAnd(
1804
                                    $this->dbHandler->quoteColumn('language_mask'),
1805
                                    ':languageId'
1806
                                ),
1807
                                0
1808
                            )
1809
                        ),
1810
                        $languageQuery->expr->bitOr(
1811
                            ':languageId',
1812
                            1
1813
                        ),
1814
                    ],
1815
                    ':languageId'
1816
                )
1817
            )
1818
            ->from('ezcontentobject')
1819
            ->where(
1820
                $languageQuery->expr->eq(
1821
                    'id',
1822
                    ':contentId'
1823
                )
1824
            );
1825
1826
        return $languageQuery;
1827
    }
1828
1829
    /**
1830
     * Deletes the actual content object referred to by $contentId.
1831
     *
1832
     * @param int $contentId
1833
     */
1834
    public function deleteContent($contentId)
1835
    {
1836
        $query = $this->dbHandler->createDeleteQuery();
1837
        $query->deleteFrom('ezcontentobject')
1838
            ->where(
1839
                $query->expr->eq(
1840
                    $this->dbHandler->quoteColumn('id'),
1841
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1842
                )
1843
            );
1844
1845
        $query->prepare()->execute();
1846
    }
1847
1848
    /**
1849
     * Loads relations from $contentId to published content, optionally only from $contentVersionNo.
1850
     *
1851
     * $relationType can also be filtered.
1852
     *
1853
     * @param int $contentId
1854
     * @param int $contentVersionNo
1855
     * @param int $relationType
1856
     *
1857
     * @return string[][] array of relation data
1858
     */
1859
    public function loadRelations($contentId, $contentVersionNo = null, $relationType = null)
1860
    {
1861
        $query = $this->queryBuilder->createRelationFindQuery();
1862
        $query->innerJoin(
1863
            $query->alias(
1864
                $this->dbHandler->quoteTable('ezcontentobject'),
1865
                'ezcontentobject_to'
1866
            ),
1867
            $query->expr->lAnd(
1868
                $query->expr->eq(
1869
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1870
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject_to')
1871
                ),
1872
                $query->expr->eq(
1873
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_to'),
1874
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1875
                )
1876
            )
1877
        )->where(
1878
            $query->expr->eq(
1879
                $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1880
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1881
            )
1882
        );
1883
1884
        // source version number
1885
        if (isset($contentVersionNo)) {
1886
            $query->where(
1887
                $query->expr->eq(
1888
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1889
                    $query->bindValue($contentVersionNo, null, \PDO::PARAM_INT)
1890
                )
1891
            );
1892
        } else { // from published version only
1893
            $query->from(
1894
                $this->dbHandler->quoteTable('ezcontentobject')
1895
            )->where(
1896
                $query->expr->lAnd(
1897
                    $query->expr->eq(
1898
                        $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1899
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1900
                    ),
1901
                    $query->expr->eq(
1902
                        $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1903
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1904
                    )
1905
                )
1906
            );
1907
        }
1908
1909
        // relation type
1910
        if (isset($relationType)) {
1911
            $query->where(
1912
                $query->expr->gt(
1913
                    $query->expr->bitAnd(
1914
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1915
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1916
                    ),
1917
                    0
1918
                )
1919
            );
1920
        }
1921
1922
        $statement = $query->prepare();
1923
        $statement->execute();
1924
1925
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1926
    }
1927
1928
    /**
1929
     * {@inheritdoc}
1930
     */
1931
    public function countReverseRelations(int $toContentId, ?int $relationType = null): int
1932
    {
1933
        $platform = $this->connection->getDatabasePlatform();
1934
        $query = $this->connection->createQueryBuilder();
1935
        $expr = $query->expr();
1936
        $query
1937
            ->select($platform->getCountExpression('l.id'))
1938
            ->from('ezcontentobject_link', 'l')
1939
            ->innerJoin(
1940
                'l',
1941
                'ezcontentobject',
1942
                'c',
1943
                $expr->andX(
1944
                    $expr->eq('l.from_contentobject_id', 'c.id'),
1945
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
1946
                    $expr->eq('c.status', 1)
1947
                )
1948
            )
1949
            ->where(
1950
                $expr->eq('l.to_contentobject_id', ':toContentId')
1951
            )
1952
            ->setParameter(':toContentId', $toContentId, ParameterType::INTEGER);
1953
1954
        // relation type
1955
        if ($relationType !== null) {
1956
            $query->andWhere(
1957
                $expr->gt(
1958
                    $platform->getBitAndComparisonExpression('l.relation_type', $relationType),
1959
                    0
1960
                )
1961
            );
1962
        }
1963
1964
        return (int) $query->execute()->fetchColumn();
1965
    }
1966
1967
    /**
1968
     * Loads data that related to $toContentId.
1969
     *
1970
     * @param int $toContentId
1971
     * @param int $relationType
1972
     *
1973
     * @return mixed[][] Content data, array structured like {@see \eZ\Publish\Core\Persistence\Legacy\Content\Gateway::load()}
1974
     */
1975
    public function loadReverseRelations($toContentId, $relationType = null)
1976
    {
1977
        $query = $this->queryBuilder->createRelationFindQuery();
1978
        $query->where(
1979
            $query->expr->eq(
1980
                $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1981
                $query->bindValue($toContentId, null, \PDO::PARAM_INT)
1982
            )
1983
        );
1984
1985
        // ezcontentobject join
1986
        $query->from(
1987
            $this->dbHandler->quoteTable('ezcontentobject')
1988
        )->where(
1989
            $query->expr->lAnd(
1990
                $query->expr->eq(
1991
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1992
                    $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1993
                ),
1994
                $query->expr->eq(
1995
                    $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1996
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1997
                ),
1998
                $query->expr->eq(
1999
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject'),
2000
                    $query->bindValue(1, null, \PDO::PARAM_INT)
2001
                )
2002
            )
2003
        );
2004
2005
        // relation type
2006
        if (isset($relationType)) {
2007
            $query->where(
2008
                $query->expr->gt(
2009
                    $query->expr->bitAnd(
2010
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
2011
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
2012
                    ),
2013
                    0
2014
                )
2015
            );
2016
        }
2017
2018
        $statement = $query->prepare();
2019
2020
        $statement->execute();
2021
2022
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
2023
    }
2024
2025
    /**
2026
     * {@inheritdoc}
2027
     */
2028
    public function listReverseRelations(int $toContentId, int $offset = 0, int $limit = -1, ?int $relationType = null): array
2029
    {
2030
        $platform = $this->connection->getDatabasePlatform();
2031
        $query = $this->createRelationFindQuery();
2032
        $expr = $query->expr();
2033
        $query
2034
            ->innerJoin(
2035
                'l',
2036
                'ezcontentobject',
2037
                'c',
2038
                $expr->andX(
2039
                    $expr->eq('l.from_contentobject_id', 'c.id'),
2040
                    $expr->eq('l.from_contentobject_version', 'c.current_version'),
2041
                    $expr->eq('c.status', ContentInfo::STATUS_PUBLISHED)
2042
                )
2043
            )
2044
            ->where(
2045
                $expr->eq('l.to_contentobject_id', ':toContentId')
2046
            )
2047
            ->setParameter(':toContentId', $toContentId, ParameterType::INTEGER);
2048
2049
        // relation type
2050
        if ($relationType !== null) {
2051
            $query->andWhere(
2052
                $expr->gt(
2053
                    $platform->getBitAndComparisonExpression('l.relation_type', $relationType),
2054
                    0
2055
                )
2056
            );
2057
        }
2058
        $query->setFirstResult($offset);
2059
        if ($limit > 0) {
2060
            $query->setMaxResults($limit);
2061
        }
2062
        $query->orderBy('l.id', 'DESC');
2063
2064
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
2065
    }
2066
2067
    /**
2068
     * Inserts a new relation database record.
2069
     *
2070
     * @param \eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct $createStruct
2071
     *
2072
     * @return int ID the inserted ID
2073
     */
2074
    public function insertRelation(RelationCreateStruct $createStruct)
2075
    {
2076
        $q = $this->dbHandler->createInsertQuery();
2077
        $q->insertInto(
2078
            $this->dbHandler->quoteTable('ezcontentobject_link')
2079
        )->set(
2080
            $this->dbHandler->quoteColumn('id'),
2081
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_link', 'id')
2082
        )->set(
2083
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
2084
            $q->bindValue((int)$createStruct->sourceFieldDefinitionId, null, \PDO::PARAM_INT)
2085
        )->set(
2086
            $this->dbHandler->quoteColumn('from_contentobject_id'),
2087
            $q->bindValue($createStruct->sourceContentId, null, \PDO::PARAM_INT)
2088
        )->set(
2089
            $this->dbHandler->quoteColumn('from_contentobject_version'),
2090
            $q->bindValue($createStruct->sourceContentVersionNo, null, \PDO::PARAM_INT)
2091
        )->set(
2092
            $this->dbHandler->quoteColumn('relation_type'),
2093
            $q->bindValue($createStruct->type, null, \PDO::PARAM_INT)
2094
        )->set(
2095
            $this->dbHandler->quoteColumn('to_contentobject_id'),
2096
            $q->bindValue($createStruct->destinationContentId, null, \PDO::PARAM_INT)
2097
        );
2098
2099
        $q->prepare()->execute();
2100
2101
        return (int)$this->dbHandler->lastInsertId(
2102
            $this->dbHandler->getSequenceName('ezcontentobject_link', 'id')
2103
        );
2104
    }
2105
2106
    /**
2107
     * Deletes the relation with the given $relationId.
2108
     *
2109
     * @param int $relationId
2110
     * @param int $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
2111
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
2112
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
2113
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
2114
     */
2115
    public function deleteRelation($relationId, $type)
2116
    {
2117
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
2118
        // existing relation type by given $relationId for comparison
2119
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
2120
        $query = $this->dbHandler->createSelectQuery();
2121
        $query->select(
2122
            $this->dbHandler->quoteColumn('relation_type')
2123
        )->from(
2124
            $this->dbHandler->quoteTable('ezcontentobject_link')
2125
        )->where(
2126
            $query->expr->eq(
2127
                $this->dbHandler->quoteColumn('id'),
2128
                $query->bindValue($relationId, null, \PDO::PARAM_INT)
2129
            )
2130
        );
2131
2132
        $statement = $query->prepare();
2133
        $statement->execute();
2134
        $loadedRelationType = $statement->fetchColumn();
2135
2136
        if (!$loadedRelationType) {
2137
            return;
2138
        }
2139
2140
        // If relation type matches then delete
2141
        if ($loadedRelationType == $type) {
2142
            /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
2143
            $query = $this->dbHandler->createDeleteQuery();
2144
            $query->deleteFrom(
2145
                'ezcontentobject_link'
2146
            )->where(
2147
                $query->expr->eq(
2148
                    $this->dbHandler->quoteColumn('id'),
2149
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2150
                )
2151
            );
2152
2153
            $query->prepare()->execute();
2154
        } elseif ($loadedRelationType & $type) { // If relation type is composite update bitmask
2155
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
2156
            $query = $this->dbHandler->createUpdateQuery();
2157
            $query->update(
2158
                $this->dbHandler->quoteTable('ezcontentobject_link')
2159
            )->set(
2160
                $this->dbHandler->quoteColumn('relation_type'),
2161
                $query->expr->bitAnd(
2162
                    $this->dbHandler->quoteColumn('relation_type'),
2163
                    $query->bindValue(~$type, null, \PDO::PARAM_INT)
2164
                )
2165
            )->where(
2166
                $query->expr->eq(
2167
                    $this->dbHandler->quoteColumn('id'),
2168
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2169
                )
2170
            );
2171
2172
            $query->prepare()->execute();
2173
        } else {
2174
            // No match, do nothing
2175
        }
2176
    }
2177
2178
    /**
2179
     * Returns all Content IDs for a given $contentTypeId.
2180
     *
2181
     * @param int $contentTypeId
2182
     *
2183
     * @return int[]
2184
     */
2185
    public function getContentIdsByContentTypeId($contentTypeId)
2186
    {
2187
        $query = $this->dbHandler->createSelectQuery();
2188
        $query
2189
            ->select($this->dbHandler->quoteColumn('id'))
2190
            ->from($this->dbHandler->quoteTable('ezcontentobject'))
2191
            ->where(
2192
                $query->expr->eq(
2193
                    $this->dbHandler->quoteColumn('contentclass_id'),
2194
                    $query->bindValue($contentTypeId, null, PDO::PARAM_INT)
2195
                )
2196
            );
2197
2198
        $statement = $query->prepare();
2199
        $statement->execute();
2200
2201
        return $statement->fetchAll(PDO::FETCH_COLUMN);
2202
    }
2203
2204
    /**
2205
     * Load name data for set of content id's and corresponding version number.
2206
     *
2207
     * @param array[] $rows array of hashes with 'id' and 'version' to load names for
2208
     *
2209
     * @return array
2210
     */
2211
    public function loadVersionedNameData($rows)
2212
    {
2213
        $query = $this->queryBuilder->createNamesQuery();
2214
        $conditions = [];
2215
        foreach ($rows as $row) {
2216
            $conditions[] = $query->expr->lAnd(
2217
                $query->expr->eq(
2218
                    $this->dbHandler->quoteColumn('contentobject_id'),
2219
                    $query->bindValue($row['id'], null, \PDO::PARAM_INT)
2220
                ),
2221
                $query->expr->eq(
2222
                    $this->dbHandler->quoteColumn('content_version'),
2223
                    $query->bindValue($row['version'], null, \PDO::PARAM_INT)
2224
                )
2225
            );
2226
        }
2227
2228
        $query->where($query->expr->lOr($conditions));
2229
        $stmt = $query->prepare();
2230
        $stmt->execute();
2231
2232
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
2233
    }
2234
2235
    /**
2236
     * Batch method for copying all relation meta data for copied Content object.
2237
     *
2238
     * {@inheritdoc}
2239
     *
2240
     * @param int $originalContentId
2241
     * @param int $copiedContentId
2242
     * @param int|null $versionNo If specified only copy for a given version number, otherwise all.
2243
     */
2244
    public function copyRelations($originalContentId, $copiedContentId, $versionNo = null)
2245
    {
2246
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
2247
        $sql = 'INSERT INTO ezcontentobject_link ( contentclassattribute_id, from_contentobject_id, from_contentobject_version, relation_type, to_contentobject_id )
2248
                SELECT  L2.contentclassattribute_id, :copied_id, L2.from_contentobject_version, L2.relation_type, L2.to_contentobject_id
2249
                FROM    ezcontentobject_link AS L2
2250
                WHERE   L2.from_contentobject_id = :original_id';
2251
2252
        if ($versionNo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $versionNo of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2253
            $stmt = $this->connection->prepare($sql . ' AND L2.from_contentobject_version = :version');
2254
            $stmt->bindValue('version', $versionNo, PDO::PARAM_INT);
2255
        } else {
2256
            $stmt = $this->connection->prepare($sql);
2257
        }
2258
2259
        $stmt->bindValue('original_id', $originalContentId, PDO::PARAM_INT);
2260
        $stmt->bindValue('copied_id', $copiedContentId, PDO::PARAM_INT);
2261
2262
        $stmt->execute();
2263
    }
2264
2265
    /**
2266
     * Remove the specified translation from the Content Object Version.
2267
     *
2268
     * @param int $contentId
2269
     * @param string $languageCode language code of the translation
2270
     *
2271
     * @throws \Doctrine\DBAL\DBALException
2272
     */
2273
    public function deleteTranslationFromContent($contentId, $languageCode)
2274
    {
2275
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2276
2277
        $this->connection->beginTransaction();
2278
        try {
2279
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
2280
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
2281
            $this->deleteTranslationFromContentObject($contentId, $language->id);
2282
2283
            $this->connection->commit();
2284
        } catch (DBALException $e) {
2285
            $this->connection->rollBack();
2286
            throw $e;
2287
        }
2288
    }
2289
2290
    /**
2291
     * Delete Content fields (attributes) for the given Translation.
2292
     * If $versionNo is given, fields for that Version only will be deleted.
2293
     *
2294
     * @param string $languageCode
2295
     * @param int $contentId
2296
     * @param int $versionNo (optional) filter by versionNo
2297
     */
2298
    public function deleteTranslatedFields($languageCode, $contentId, $versionNo = null)
2299
    {
2300
        $query = $this->connection->createQueryBuilder();
2301
        $query
2302
            ->delete('ezcontentobject_attribute')
2303
            ->where('contentobject_id = :contentId')
2304
            ->andWhere('language_code = :languageCode')
2305
            ->setParameters(
2306
                [
2307
                    ':contentId' => $contentId,
2308
                    ':languageCode' => $languageCode,
2309
                ]
2310
            )
2311
        ;
2312
2313
        if (null !== $versionNo) {
2314
            $query
2315
                ->andWhere('version = :versionNo')
2316
                ->setParameter(':versionNo', $versionNo)
2317
            ;
2318
        }
2319
2320
        $query->execute();
2321
    }
2322
2323
    /**
2324
     * Delete the specified Translation from the given Version.
2325
     *
2326
     * @param int $contentId
2327
     * @param int $versionNo
2328
     * @param string $languageCode
2329
     *
2330
     * @throws \Doctrine\DBAL\DBALException
2331
     */
2332
    public function deleteTranslationFromVersion($contentId, $versionNo, $languageCode)
2333
    {
2334
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2335
2336
        $this->connection->beginTransaction();
2337
        try {
2338
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
2339
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
2340
2341
            $this->connection->commit();
2342
        } catch (DBALException $e) {
2343
            $this->connection->rollBack();
2344
            throw $e;
2345
        }
2346
    }
2347
2348
    /**
2349
     * Delete translation from the ezcontentobject_name table.
2350
     *
2351
     * @param int $contentId
2352
     * @param string $languageCode
2353
     * @param int $versionNo optional, if specified, apply to this Version only.
2354
     */
2355
    private function deleteTranslationFromContentNames($contentId, $languageCode, $versionNo = null)
2356
    {
2357
        $query = $this->connection->createQueryBuilder();
2358
        $query
2359
            ->delete('ezcontentobject_name')
2360
            ->where('contentobject_id=:contentId')
2361
            ->andWhere('real_translation=:languageCode')
2362
            ->setParameters(
2363
                [
2364
                    ':languageCode' => $languageCode,
2365
                    ':contentId' => $contentId,
2366
                ]
2367
            )
2368
        ;
2369
2370
        if (null !== $versionNo) {
2371
            $query
2372
                ->andWhere('content_version = :versionNo')
2373
                ->setParameter(':versionNo', $versionNo)
2374
            ;
2375
        }
2376
2377
        $query->execute();
2378
    }
2379
2380
    /**
2381
     * Remove language from language_mask of ezcontentobject.
2382
     *
2383
     * @param int $contentId
2384
     * @param int $languageId
2385
     *
2386
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2387
     */
2388
    private function deleteTranslationFromContentObject($contentId, $languageId)
2389
    {
2390
        $query = $this->connection->createQueryBuilder();
2391
        $query->update('ezcontentobject')
2392
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2393
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2394
            ->set('modified', ':now')
2395
            ->where('id = :contentId')
2396
            ->andWhere(
2397
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2398
                $query->expr()->andX(
2399
                    'language_mask & ~ ' . $languageId . ' <> 0',
2400
                    'language_mask & ~ ' . $languageId . ' <> 1'
2401
                )
2402
            )
2403
            ->setParameter(':now', time())
2404
            ->setParameter(':contentId', $contentId)
2405
        ;
2406
2407
        $rowCount = $query->execute();
2408
2409
        // no rows updated means that most likely somehow it was the last remaining translation
2410
        if ($rowCount === 0) {
2411
            throw new BadStateException(
2412
                '$languageCode',
2413
                'Specified translation is the only one Content Object Version has'
2414
            );
2415
        }
2416
    }
2417
2418
    /**
2419
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
2420
     * if it matches the removed one.
2421
     *
2422
     * @param int $contentId
2423
     * @param int $languageId
2424
     * @param int $versionNo optional, if specified, apply to this Version only.
2425
     *
2426
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2427
     */
2428
    private function deleteTranslationFromContentVersions($contentId, $languageId, $versionNo = null)
2429
    {
2430
        $query = $this->connection->createQueryBuilder();
2431
        $query->update('ezcontentobject_version')
2432
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2433
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2434
            ->set('modified', ':now')
2435
            // update initial_language_id only if it matches removed translation languageId
2436
            ->set(
2437
                'initial_language_id',
2438
                'CASE WHEN initial_language_id = :languageId ' .
2439
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
2440
                'ELSE initial_language_id END'
2441
            )
2442
            ->where('contentobject_id = :contentId')
2443
            ->andWhere(
2444
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2445
                $query->expr()->andX(
2446
                    'language_mask & ~ ' . $languageId . ' <> 0',
2447
                    'language_mask & ~ ' . $languageId . ' <> 1'
2448
                )
2449
            )
2450
            ->setParameter(':now', time())
2451
            ->setParameter(':contentId', $contentId)
2452
            ->setParameter(':languageId', $languageId)
2453
        ;
2454
2455
        if (null !== $versionNo) {
2456
            $query
2457
                ->andWhere('version = :versionNo')
2458
                ->setParameter(':versionNo', $versionNo)
2459
            ;
2460
        }
2461
2462
        $rowCount = $query->execute();
2463
2464
        // no rows updated means that most likely somehow it was the last remaining translation
2465
        if ($rowCount === 0) {
2466
            throw new BadStateException(
2467
                '$languageCode',
2468
                'Specified translation is the only one Content Object Version has'
2469
            );
2470
        }
2471
    }
2472
2473
    /**
2474
     * Get query builder for content version objects, used for version loading w/o fields.
2475
     *
2476
     * Creates a select query with all necessary joins to fetch a complete
2477
     * content object. Does not apply any WHERE conditions, and does not contain
2478
     * name data as it will lead to large result set {@see createNamesQuery}.
2479
     *
2480
     * @return \Doctrine\DBAL\Query\QueryBuilder
2481
     */
2482
    private function createVersionInfoFindQueryBuilder(): DoctrineQueryBuilder
2483
    {
2484
        $query = $this->connection->createQueryBuilder();
2485
        $expr = $query->expr();
2486
2487
        $query
2488
            ->select(
2489
                'v.id AS ezcontentobject_version_id',
2490
                'v.version AS ezcontentobject_version_version',
2491
                'v.modified AS ezcontentobject_version_modified',
2492
                'v.creator_id AS ezcontentobject_version_creator_id',
2493
                'v.created AS ezcontentobject_version_created',
2494
                'v.status AS ezcontentobject_version_status',
2495
                'v.contentobject_id AS ezcontentobject_version_contentobject_id',
2496
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
2497
                'v.language_mask AS ezcontentobject_version_language_mask',
2498
                // Content main location
2499
                't.main_node_id AS ezcontentobject_tree_main_node_id',
2500
                // Content object
2501
                // @todo: remove ezcontentobject.d from query as it duplicates ezcontentobject_version.contentobject_id
2502
                'c.id AS ezcontentobject_id',
2503
                'c.contentclass_id AS ezcontentobject_contentclass_id',
2504
                'c.section_id AS ezcontentobject_section_id',
2505
                'c.owner_id AS ezcontentobject_owner_id',
2506
                'c.remote_id AS ezcontentobject_remote_id',
2507
                'c.current_version AS ezcontentobject_current_version',
2508
                'c.initial_language_id AS ezcontentobject_initial_language_id',
2509
                'c.modified AS ezcontentobject_modified',
2510
                'c.published AS ezcontentobject_published',
2511
                'c.status AS ezcontentobject_status',
2512
                'c.name AS ezcontentobject_name',
2513
                'c.language_mask AS ezcontentobject_language_mask',
2514
                'c.is_hidden AS ezcontentobject_is_hidden'
2515
            )
2516
            ->from('ezcontentobject_version', 'v')
2517
            ->innerJoin(
2518
                'v',
2519
                'ezcontentobject',
2520
                'c',
2521
                $expr->eq('c.id', 'v.contentobject_id')
2522
            )
2523
            ->leftJoin(
2524
                'v',
2525
                'ezcontentobject_tree',
2526
                't',
2527
                $expr->andX(
2528
                    $expr->eq('t.contentobject_id', 'v.contentobject_id'),
2529
                    $expr->eq('t.main_node_id', 't.node_id')
2530
                )
2531
            );
2532
2533
        return $query;
2534
    }
2535
2536
    /**
2537
     * Creates a select query for content relations.
2538
     *
2539
     * @return \Doctrine\DBAL\Query\QueryBuilder
2540
     */
2541
    public function createRelationFindQuery(): DoctrineQueryBuilder
2542
    {
2543
        $query = $this->connection->createQueryBuilder();
2544
        $query
2545
            ->select(
2546
                'l.id AS ezcontentobject_link_id',
2547
                'l.contentclassattribute_id AS ezcontentobject_link_contentclassattribute_id',
2548
                'l.from_contentobject_id AS ezcontentobject_link_from_contentobject_id',
2549
                'l.from_contentobject_version AS ezcontentobject_link_from_contentobject_version',
2550
                'l.relation_type AS ezcontentobject_link_relation_type',
2551
                'l.to_contentobject_id AS ezcontentobject_link_to_contentobject_id'
2552
            )
2553
            ->from(
2554
                'ezcontentobject_link', 'l'
2555
            );
2556
2557
        return $query;
2558
    }
2559
}
2560