Completed
Push — parallel_test_fork ( b1fcc3...0461b5 )
by
unknown
33:11 queued 13:37
created

DoctrineDatabase::markContentAsPublished()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 13
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 13
loc 13
rs 9.8333
c 0
b 0
f 0
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
     * @deprecated Start to use DBAL $connection instead.
50
     */
51
    protected $dbHandler;
52
53
    /**
54
     * The native Doctrine connection.
55
     *
56
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
57
     *
58
     * @var \Doctrine\DBAL\Connection
59
     */
60
    protected $connection;
61
62
    /**
63
     * Query builder.
64
     *
65
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
66
     */
67
    protected $queryBuilder;
68
69
    /**
70
     * Caching language handler.
71
     *
72
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
73
     */
74
    protected $languageHandler;
75
76
    /**
77
     * Language mask generator.
78
     *
79
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
80
     */
81
    protected $languageMaskGenerator;
82
83
    /**
84
     * Creates a new gateway based on $db.
85
     *
86
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $db
87
     * @param \Doctrine\DBAL\Connection $connection
88
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder $queryBuilder
89
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
90
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
91
     */
92
    public function __construct(
93
        DatabaseHandler $db,
94
        Connection $connection,
95
        QueryBuilder $queryBuilder,
96
        LanguageHandler $languageHandler,
97
        LanguageMaskGenerator $languageMaskGenerator
98
    ) {
99
        $this->dbHandler = $db;
100
        $this->connection = $connection;
101
        $this->queryBuilder = $queryBuilder;
102
        $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...
103
        $this->languageMaskGenerator = $languageMaskGenerator;
104
    }
105
106
    /**
107
     * Get context definition for external storage layers.
108
     *
109
     * @return array
110
     */
111
    public function getContext()
112
    {
113
        return [
114
            'identifier' => 'LegacyStorage',
115
            'connection' => $this->dbHandler,
116
        ];
117
    }
118
119
    /**
120
     * Inserts a new content object.
121
     *
122
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct
123
     * @param mixed $currentVersionNo
124
     *
125
     * @return int ID
126
     */
127
    public function insertContentObject(CreateStruct $struct, $currentVersionNo = 1)
128
    {
129
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
130
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
131
132
        if (isset($struct->name[$initialLanguageCode])) {
133
            $name = $struct->name[$initialLanguageCode];
134
        } else {
135
            $name = '';
136
        }
137
138
        $q = $this->dbHandler->createInsertQuery();
139
        $q->insertInto(
140
            $this->dbHandler->quoteTable('ezcontentobject')
141
        )->set(
142
            $this->dbHandler->quoteColumn('id'),
143
            $this->dbHandler->getAutoIncrementValue('ezcontentobject', 'id')
144
        )->set(
145
            $this->dbHandler->quoteColumn('current_version'),
146
            $q->bindValue($currentVersionNo, null, \PDO::PARAM_INT)
147
        )->set(
148
            $this->dbHandler->quoteColumn('name'),
149
            $q->bindValue($name, null, \PDO::PARAM_STR)
150
        )->set(
151
            $this->dbHandler->quoteColumn('contentclass_id'),
152
            $q->bindValue($struct->typeId, null, \PDO::PARAM_INT)
153
        )->set(
154
            $this->dbHandler->quoteColumn('section_id'),
155
            $q->bindValue($struct->sectionId, null, \PDO::PARAM_INT)
156
        )->set(
157
            $this->dbHandler->quoteColumn('owner_id'),
158
            $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
159
        )->set(
160
            $this->dbHandler->quoteColumn('initial_language_id'),
161
            $q->bindValue($initialLanguageId, null, \PDO::PARAM_INT)
162
        )->set(
163
            $this->dbHandler->quoteColumn('remote_id'),
164
            $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
165
        )->set(
166
            $this->dbHandler->quoteColumn('modified'),
167
            $q->bindValue(0, null, \PDO::PARAM_INT)
168
        )->set(
169
            $this->dbHandler->quoteColumn('published'),
170
            $q->bindValue(0, null, \PDO::PARAM_INT)
171
        )->set(
172
            $this->dbHandler->quoteColumn('status'),
173
            $q->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
174
        )->set(
175
            $this->dbHandler->quoteColumn('language_mask'),
176
            $q->bindValue(
177
                $this->generateLanguageMask(
178
                    $struct->fields,
179
                    $initialLanguageCode,
180
                    $struct->alwaysAvailable
181
                ),
182
                null,
183
                \PDO::PARAM_INT
184
            )
185
        );
186
187
        $q->prepare()->execute();
188
189
        return $this->dbHandler->lastInsertId(
190
            $this->dbHandler->getSequenceName('ezcontentobject', 'id')
191
        );
192
    }
193
194
    /**
195
     * Generates a language mask for $fields.
196
     *
197
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
198
     * @param string $initialLanguageCode
199
     * @param bool $isAlwaysAvailable
200
     *
201
     * @return int
202
     */
203
    protected function generateLanguageMask(array $fields, string $initialLanguageCode, bool $isAlwaysAvailable): int
204
    {
205
        $languages = [$initialLanguageCode => true];
206 View Code Duplication
        foreach ($fields as $field) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
207
            if (isset($languages[$field->languageCode])) {
208
                continue;
209
            }
210
211
            $languages[$field->languageCode] = true;
212
        }
213
214
        return $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(array_keys($languages), $isAlwaysAvailable);
215
    }
216
217
    /**
218
     * Inserts a new version.
219
     *
220
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $versionInfo
221
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
222
     *
223
     * @return int ID
224
     */
225
    public function insertVersion(VersionInfo $versionInfo, array $fields)
226
    {
227
        /** @var $q \eZ\Publish\Core\Persistence\Database\InsertQuery */
228
        $q = $this->dbHandler->createInsertQuery();
229
        $q->insertInto(
230
            $this->dbHandler->quoteTable('ezcontentobject_version')
231
        )->set(
232
            $this->dbHandler->quoteColumn('id'),
233
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_version', 'id')
234
        )->set(
235
            $this->dbHandler->quoteColumn('version'),
236
            $q->bindValue($versionInfo->versionNo, null, \PDO::PARAM_INT)
237
        )->set(
238
            $this->dbHandler->quoteColumn('modified'),
239
            $q->bindValue($versionInfo->modificationDate, null, \PDO::PARAM_INT)
240
        )->set(
241
            $this->dbHandler->quoteColumn('creator_id'),
242
            $q->bindValue($versionInfo->creatorId, null, \PDO::PARAM_INT)
243
        )->set(
244
            $this->dbHandler->quoteColumn('created'),
245
            $q->bindValue($versionInfo->creationDate, null, \PDO::PARAM_INT)
246
        )->set(
247
            $this->dbHandler->quoteColumn('status'),
248
            $q->bindValue($versionInfo->status, null, \PDO::PARAM_INT)
249
        )->set(
250
            $this->dbHandler->quoteColumn('initial_language_id'),
251
            $q->bindValue(
252
                $this->languageHandler->loadByLanguageCode($versionInfo->initialLanguageCode)->id,
253
                null,
254
                \PDO::PARAM_INT
255
            )
256
        )->set(
257
            $this->dbHandler->quoteColumn('contentobject_id'),
258
            $q->bindValue($versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
259
        )->set(
260
            // As described in field mapping document
261
            $this->dbHandler->quoteColumn('workflow_event_pos'),
262
            $q->bindValue(0, null, \PDO::PARAM_INT)
263
        )->set(
264
            $this->dbHandler->quoteColumn('language_mask'),
265
            $q->bindValue(
266
                $this->generateLanguageMask(
267
                    $fields,
268
                    $versionInfo->initialLanguageCode,
269
                    $versionInfo->contentInfo->alwaysAvailable
270
                ),
271
                null,
272
                \PDO::PARAM_INT
273
            )
274
        );
275
276
        $q->prepare()->execute();
277
278
        return $this->dbHandler->lastInsertId(
279
            $this->dbHandler->getSequenceName('ezcontentobject_version', 'id')
280
        );
281
    }
282
283
    /**
284
     * Updates an existing content identified by $contentId in respect to $struct.
285
     *
286
     * @param int $contentId
287
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $struct
288
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $prePublishVersionInfo Provided on publish
289
     */
290
    public function updateContent($contentId, MetadataUpdateStruct $struct, VersionInfo $prePublishVersionInfo = null)
291
    {
292
        $q = $this->dbHandler->createUpdateQuery();
293
        $q->update($this->dbHandler->quoteTable('ezcontentobject'));
294
295
        if (isset($struct->name)) {
296
            $q->set(
297
                $this->dbHandler->quoteColumn('name'),
298
                $q->bindValue($struct->name, null, \PDO::PARAM_STR)
299
            );
300
        }
301
        if (isset($struct->mainLanguageId)) {
302
            $q->set(
303
                $this->dbHandler->quoteColumn('initial_language_id'),
304
                $q->bindValue($struct->mainLanguageId, null, \PDO::PARAM_INT)
305
            );
306
        }
307
        if (isset($struct->modificationDate)) {
308
            $q->set(
309
                $this->dbHandler->quoteColumn('modified'),
310
                $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
311
            );
312
        }
313
        if (isset($struct->ownerId)) {
314
            $q->set(
315
                $this->dbHandler->quoteColumn('owner_id'),
316
                $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
317
            );
318
        }
319
        if (isset($struct->publicationDate)) {
320
            $q->set(
321
                $this->dbHandler->quoteColumn('published'),
322
                $q->bindValue($struct->publicationDate, null, \PDO::PARAM_INT)
323
            );
324
        }
325
        if (isset($struct->remoteId)) {
326
            $q->set(
327
                $this->dbHandler->quoteColumn('remote_id'),
328
                $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
329
            );
330
        }
331
        if ($prePublishVersionInfo !== null) {
332
            $mask = $this->languageMaskGenerator->generateLanguageMaskFromLanguageCodes(
333
                $prePublishVersionInfo->languageCodes,
334
                $struct->alwaysAvailable ?? $prePublishVersionInfo->contentInfo->alwaysAvailable
335
            );
336
337
            $q->set(
338
                $this->dbHandler->quoteColumn('language_mask'),
339
                $q->bindValue($mask, null, \PDO::PARAM_INT)
340
            );
341
        }
342
        if (isset($struct->isHidden)) {
343
            $q->set(
344
                $this->dbHandler->quoteColumn('is_hidden'),
345
                $q->bindValue($struct->isHidden, null, \PDO::PARAM_BOOL)
346
            );
347
        }
348
        $q->where(
349
            $q->expr->eq(
350
                $this->dbHandler->quoteColumn('id'),
351
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
352
            )
353
        );
354
        $q->prepare()->execute();
355
356
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
357
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
358
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
359
        }
360
    }
361
362
    /**
363
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
364
     *
365
     * @param int $contentId
366
     * @param int $versionNo
367
     * @param \eZ\Publish\SPI\Persistence\Content\UpdateStruct $struct
368
     */
369
    public function updateVersion($contentId, $versionNo, UpdateStruct $struct)
370
    {
371
        $q = $this->dbHandler->createUpdateQuery();
372
        $q->update(
373
            $this->dbHandler->quoteTable('ezcontentobject_version')
374
        )->set(
375
            $this->dbHandler->quoteColumn('creator_id'),
376
            $q->bindValue($struct->creatorId, null, \PDO::PARAM_INT)
377
        )->set(
378
            $this->dbHandler->quoteColumn('modified'),
379
            $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
380
        )->set(
381
            $this->dbHandler->quoteColumn('initial_language_id'),
382
            $q->bindValue($struct->initialLanguageId, null, \PDO::PARAM_INT)
383
        )->set(
384
            $this->dbHandler->quoteColumn('language_mask'),
385
            $q->expr->bitOr(
386
                $this->dbHandler->quoteColumn('language_mask'),
387
                $q->bindValue(
388
                    $this->generateLanguageMask(
389
                        $struct->fields,
390
                        $this->languageHandler->load($struct->initialLanguageId)->languageCode,
391
                        false
392
                    ),
393
                    null,
394
                    \PDO::PARAM_INT
395
                )
396
            )
397
        )->where(
398
            $q->expr->lAnd(
399
                $q->expr->eq(
400
                    $this->dbHandler->quoteColumn('contentobject_id'),
401
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
402
                ),
403
                $q->expr->eq(
404
                    $this->dbHandler->quoteColumn('version'),
405
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
406
                )
407
            )
408
        );
409
        $q->prepare()->execute();
410
    }
411
412
    /**
413
     * Updates "always available" flag for Content identified by $contentId, in respect to
414
     * Content's current main language and optionally new $alwaysAvailable state.
415
     *
416
     * @param int $contentId
417
     * @param bool|null $alwaysAvailable New "always available" value or null if not defined
418
     */
419
    public function updateAlwaysAvailableFlag($contentId, $alwaysAvailable = null)
420
    {
421
        // We will need to know some info on the current language mask to update the flag
422
        // everywhere needed
423
        $contentInfoRow = $this->loadContentInfo($contentId);
424
        if (!isset($alwaysAvailable)) {
425
            $alwaysAvailable = (bool)$contentInfoRow['language_mask'] & 1;
426
        }
427
428
        /** @var $q \eZ\Publish\Core\Persistence\Database\UpdateQuery */
429
        $q = $this->dbHandler->createUpdateQuery();
430
        $q
431
            ->update($this->dbHandler->quoteTable('ezcontentobject'))
432
            ->set(
433
                $this->dbHandler->quoteColumn('language_mask'),
434
                $alwaysAvailable ?
435
                    $q->expr->bitOr($this->dbHandler->quoteColumn('language_mask'), 1) :
436
                    $q->expr->bitAnd($this->dbHandler->quoteColumn('language_mask'), -2)
437
            )
438
            ->where(
439
                $q->expr->eq(
440
                    $this->dbHandler->quoteColumn('id'),
441
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
442
                )
443
            );
444
        $q->prepare()->execute();
445
446
        // Now we need to update ezcontentobject_name
447
        /** @var $qName \eZ\Publish\Core\Persistence\Database\UpdateQuery */
448
        $qName = $this->dbHandler->createUpdateQuery();
449
        $qName
450
            ->update($this->dbHandler->quoteTable('ezcontentobject_name'))
451
            ->set(
452
                $this->dbHandler->quoteColumn('language_id'),
453
                $alwaysAvailable ?
454
                    $qName->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
455
                    $qName->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
456
            )
457
            ->where(
458
                $qName->expr->lAnd(
459
                    $qName->expr->eq(
460
                        $this->dbHandler->quoteColumn('contentobject_id'),
461
                        $qName->bindValue($contentId, null, \PDO::PARAM_INT)
462
                    ),
463
                    $qName->expr->eq(
464
                        $this->dbHandler->quoteColumn('content_version'),
465
                        $qName->bindValue(
466
                            $contentInfoRow['current_version'],
467
                            null,
468
                            \PDO::PARAM_INT
469
                        )
470
                    )
471
                )
472
            );
473
        $qName->prepare()->execute();
474
475
        // Now update ezcontentobject_attribute for current version
476
        // Create update query that will be reused
477
        /** @var $qAttr \eZ\Publish\Core\Persistence\Database\UpdateQuery */
478
        $qAttr = $this->dbHandler->createUpdateQuery();
479
        $qAttr
480
            ->update($this->dbHandler->quoteTable('ezcontentobject_attribute'))
481
            ->where(
482
                $qAttr->expr->lAnd(
483
                    $qAttr->expr->eq(
484
                        $this->dbHandler->quoteColumn('contentobject_id'),
485
                        $qAttr->bindValue($contentId, null, \PDO::PARAM_INT)
486
                    ),
487
                    $qAttr->expr->eq(
488
                        $this->dbHandler->quoteColumn('version'),
489
                        $qAttr->bindValue(
490
                            $contentInfoRow['current_version'],
491
                            null,
492
                            \PDO::PARAM_INT
493
                        )
494
                    )
495
                )
496
            );
497
498
        // If there is only a single language, update all fields and return
499
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($contentInfoRow['language_mask'])) {
500
            $qAttr->set(
501
                $this->dbHandler->quoteColumn('language_id'),
502
                $alwaysAvailable ?
503
                    $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
504
                    $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
505
            );
506
            $qAttr->prepare()->execute();
507
508
            return;
509
        }
510
511
        // Otherwise:
512
        // 1. Remove always available flag on all fields
513
        $qAttr->set(
514
            $this->dbHandler->quoteColumn('language_id'),
515
            $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
516
        );
517
        $qAttr->prepare()->execute();
518
519
        // 2. If Content is always available set the flag only on fields in main language
520
        if ($alwaysAvailable) {
521
            $qAttr->set(
522
                $this->dbHandler->quoteColumn('language_id'),
523
                $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1)
524
            );
525
            $qAttr->where(
526
                $qAttr->expr->gt(
527
                    $qAttr->expr->bitAnd(
528
                        $this->dbHandler->quoteColumn('language_id'),
529
                        $qAttr->bindValue($contentInfoRow['initial_language_id'], null, PDO::PARAM_INT)
530
                    ),
531
                    $qAttr->bindValue(0, null, PDO::PARAM_INT)
532
                )
533
            );
534
            $qAttr->prepare()->execute();
535
        }
536
    }
537
538
    /**
539
     * Sets the status of the version identified by $contentId and $version to $status.
540
     *
541
     * The $status can be one of STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED
542
     *
543
     * @param int $contentId
544
     * @param int $version
545
     * @param int $status
546
     *
547
     * @return bool
548
     */
549
    public function setStatus($contentId, $version, $status)
550
    {
551
        $q = $this->dbHandler->createUpdateQuery();
552
        $q->update(
553
            $this->dbHandler->quoteTable('ezcontentobject_version')
554
        )->set(
555
            $this->dbHandler->quoteColumn('status'),
556
            $q->bindValue($status, null, \PDO::PARAM_INT)
557
        )->set(
558
            $this->dbHandler->quoteColumn('modified'),
559
            $q->bindValue(time(), null, \PDO::PARAM_INT)
560
        )->where(
561
            $q->expr->lAnd(
562
                $q->expr->eq(
563
                    $this->dbHandler->quoteColumn('contentobject_id'),
564
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
565
                ),
566
                $q->expr->eq(
567
                    $this->dbHandler->quoteColumn('version'),
568
                    $q->bindValue($version, null, \PDO::PARAM_INT)
569
                )
570
            )
571
        );
572
        $statement = $q->prepare();
573
        $statement->execute();
574
575
        if ((bool)$statement->rowCount() === false) {
576
            return false;
577
        }
578
579
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
580
            return true;
581
        }
582
583
        // If the version's status is PUBLISHED, we set the content to published status as well
584
        $q = $this->dbHandler->createUpdateQuery();
585
        $q->update(
586
            $this->dbHandler->quoteTable('ezcontentobject')
587
        )->set(
588
            $this->dbHandler->quoteColumn('status'),
589
            $q->bindValue(ContentInfo::STATUS_PUBLISHED, null, \PDO::PARAM_INT)
590
        )->set(
591
            $this->dbHandler->quoteColumn('current_version'),
592
            $q->bindValue($version, null, \PDO::PARAM_INT)
593
        )->where(
594
            $q->expr->eq(
595
                $this->dbHandler->quoteColumn('id'),
596
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
597
            )
598
        );
599
        $statement = $q->prepare();
600
        $statement->execute();
601
602
        return (bool)$statement->rowCount();
603
    }
604
605
    public function setPublishedStatus(int $contentId, int $versionNo): void
606
    {
607
        $query = $this->getSetVersionStatusQuery(
608
            $contentId,
609
            $versionNo,
610
            VersionInfo::STATUS_PUBLISHED
611
        );
612
613
        $notExistPublishedVersion = <<< HEREDOC
614
            NOT EXISTS (
615
                SELECT 1 FROM (
616
                    SELECT 1 FROM ezcontentobject_version  WHERE contentobject_id = :contentId AND status = :status 
617
                ) as V
618
            )
619
HEREDOC;
620
621
        $query->andWhere($notExistPublishedVersion);
622
        if (0 === $query->execute()) {
623
            throw new BadStateException(
624
                '$contentId', "Someone just published another Version of the Content item {$contentId}"
625
            );
626
        }
627
        $this->markContentAsPublished($contentId, $versionNo);
628
    }
629
630
    private function getSetVersionStatusQuery(
631
        int $contentId,
632
        int $versionNo,
633
        int $versionStatus
634
    ): DoctrineQueryBuilder {
635
        $query = $this->connection->createQueryBuilder();
636
        $query
637
            ->update('ezcontentobject_version')
638
            ->set('status', ':status')
639
            ->set('modified', ':modified')
640
            ->where('contentobject_id = :contentId')
641
            ->andWhere('version = :versionNo')
642
            ->setParameter('status', $versionStatus, ParameterType::INTEGER)
643
            ->setParameter('modified', time(), ParameterType::INTEGER)
644
            ->setParameter('contentId', $contentId, ParameterType::INTEGER)
645
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER);
646
647
        return $query;
648
    }
649
650 View Code Duplication
    private function markContentAsPublished(int $contentId, int $versionNo): void
651
    {
652
        $query = $this->connection->createQueryBuilder();
653
        $query
654
            ->update('ezcontentobject')
655
            ->set('status', ':status')
656
            ->set('current_version', ':versionNo')
657
            ->where('id =:contentId')
658
            ->setParameter('status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
659
            ->setParameter('versionNo', $versionNo, ParameterType::INTEGER)
660
            ->setParameter('contentId', $contentId, ParameterType::INTEGER);
661
        $query->execute();
662
    }
663
664
    /**
665
     * Inserts a new field.
666
     *
667
     * Only used when a new field is created (i.e. a new object or a field in a
668
     * new language!). After that, field IDs need to stay the same, only the
669
     * version number changes.
670
     *
671
     * @param \eZ\Publish\SPI\Persistence\Content $content
672
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
673
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
674
     *
675
     * @return int ID
676
     */
677
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value)
678
    {
679
        $q = $this->dbHandler->createInsertQuery();
680
681
        $this->setInsertFieldValues($q, $content, $field, $value);
682
683
        // Insert with auto increment ID
684
        $q->set(
685
            $this->dbHandler->quoteColumn('id'),
686
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_attribute', 'id')
687
        );
688
689
        $q->prepare()->execute();
690
691
        return $this->dbHandler->lastInsertId(
692
            $this->dbHandler->getSequenceName('ezcontentobject_attribute', 'id')
693
        );
694
    }
695
696
    /**
697
     * Inserts an existing field.
698
     *
699
     * Used to insert a field with an exsting ID but a new version number.
700
     *
701
     * @param Content $content
702
     * @param Field $field
703
     * @param StorageFieldValue $value
704
     */
705
    public function insertExistingField(Content $content, Field $field, StorageFieldValue $value)
706
    {
707
        $q = $this->dbHandler->createInsertQuery();
708
709
        $this->setInsertFieldValues($q, $content, $field, $value);
710
711
        $q->set(
712
            $this->dbHandler->quoteColumn('id'),
713
            $q->bindValue($field->id, null, \PDO::PARAM_INT)
714
        );
715
716
        $q->prepare()->execute();
717
    }
718
719
    /**
720
     * Sets field (ezcontentobject_attribute) values to the given query.
721
     *
722
     * @param \eZ\Publish\Core\Persistence\Database\InsertQuery $q
723
     * @param Content $content
724
     * @param Field $field
725
     * @param StorageFieldValue $value
726
     */
727
    protected function setInsertFieldValues(InsertQuery $q, Content $content, Field $field, StorageFieldValue $value)
728
    {
729
        $q->insertInto(
730
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
731
        )->set(
732
            $this->dbHandler->quoteColumn('contentobject_id'),
733
            $q->bindValue($content->versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
734
        )->set(
735
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
736
            $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
737
        )->set(
738
            $this->dbHandler->quoteColumn('data_type_string'),
739
            $q->bindValue($field->type)
740
        )->set(
741
            $this->dbHandler->quoteColumn('language_code'),
742
            $q->bindValue($field->languageCode)
743
        )->set(
744
            $this->dbHandler->quoteColumn('version'),
745
            $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
746
        )->set(
747
            $this->dbHandler->quoteColumn('data_float'),
748
            $q->bindValue($value->dataFloat)
749
        )->set(
750
            $this->dbHandler->quoteColumn('data_int'),
751
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
752
        )->set(
753
            $this->dbHandler->quoteColumn('data_text'),
754
            $q->bindValue($value->dataText)
755
        )->set(
756
            $this->dbHandler->quoteColumn('sort_key_int'),
757
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
758
        )->set(
759
            $this->dbHandler->quoteColumn('sort_key_string'),
760
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
761
        )->set(
762
            $this->dbHandler->quoteColumn('language_id'),
763
            $q->bindValue(
764
                $this->languageMaskGenerator->generateLanguageIndicator(
765
                    $field->languageCode,
766
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
767
                ),
768
                null,
769
                \PDO::PARAM_INT
770
            )
771
        );
772
    }
773
774
    /**
775
     * Checks if $languageCode is always available in $content.
776
     *
777
     * @param \eZ\Publish\SPI\Persistence\Content $content
778
     * @param string $languageCode
779
     *
780
     * @return bool
781
     */
782
    protected function isLanguageAlwaysAvailable(Content $content, $languageCode)
783
    {
784
        return
785
            $content->versionInfo->contentInfo->alwaysAvailable &&
786
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
787
        ;
788
    }
789
790
    /**
791
     * Updates an existing field.
792
     *
793
     * @param Field $field
794
     * @param StorageFieldValue $value
795
     */
796
    public function updateField(Field $field, StorageFieldValue $value)
797
    {
798
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
799
        // cannot change on update
800
        $q = $this->dbHandler->createUpdateQuery();
801
        $this->setFieldUpdateValues($q, $value);
802
        $q->where(
803
            $q->expr->lAnd(
804
                $q->expr->eq(
805
                    $this->dbHandler->quoteColumn('id'),
806
                    $q->bindValue($field->id, null, \PDO::PARAM_INT)
807
                ),
808
                $q->expr->eq(
809
                    $this->dbHandler->quoteColumn('version'),
810
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
811
                )
812
            )
813
        );
814
        $q->prepare()->execute();
815
    }
816
817
    /**
818
     * Sets update fields for $value on $q.
819
     *
820
     * @param \eZ\Publish\Core\Persistence\Database\UpdateQuery $q
821
     * @param StorageFieldValue $value
822
     */
823
    protected function setFieldUpdateValues(UpdateQuery $q, StorageFieldValue $value)
824
    {
825
        $q->update(
826
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
827
        )->set(
828
            $this->dbHandler->quoteColumn('data_float'),
829
            $q->bindValue($value->dataFloat)
830
        )->set(
831
            $this->dbHandler->quoteColumn('data_int'),
832
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
833
        )->set(
834
            $this->dbHandler->quoteColumn('data_text'),
835
            $q->bindValue($value->dataText)
836
        )->set(
837
            $this->dbHandler->quoteColumn('sort_key_int'),
838
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
839
        )->set(
840
            $this->dbHandler->quoteColumn('sort_key_string'),
841
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
842
        );
843
    }
844
845
    /**
846
     * Updates an existing, non-translatable field.
847
     *
848
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
849
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
850
     * @param int $contentId
851
     */
852
    public function updateNonTranslatableField(
853
        Field $field,
854
        StorageFieldValue $value,
855
        $contentId
856
    ) {
857
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
858
        // cannot change on update
859
        $q = $this->dbHandler->createUpdateQuery();
860
        $this->setFieldUpdateValues($q, $value);
861
        $q->where(
862
            $q->expr->lAnd(
863
                $q->expr->eq(
864
                    $this->dbHandler->quoteColumn('contentclassattribute_id'),
865
                    $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
866
                ),
867
                $q->expr->eq(
868
                    $this->dbHandler->quoteColumn('contentobject_id'),
869
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
870
                ),
871
                $q->expr->eq(
872
                    $this->dbHandler->quoteColumn('version'),
873
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
874
                )
875
            )
876
        );
877
        $q->prepare()->execute();
878
    }
879
880
    /**
881
     * {@inheritdoc}
882
     */
883
    public function load($contentId, $version = null, array $translations = null)
884
    {
885
        return $this->internalLoadContent([$contentId], $version, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 883 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...
886
    }
887
888
    /**
889
     * {@inheritdoc}
890
     */
891
    public function loadContentList(array $contentIds, array $translations = null): array
892
    {
893
        return $this->internalLoadContent($contentIds, null, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 891 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...
894
    }
895
896
    /**
897
     * @see load(), loadContentList()
898
     *
899
     * @param array $contentIds
900
     * @param int|null $version
901
     * @param string[]|null $translations
902
     *
903
     * @return array
904
     */
905
    private function internalLoadContent(array $contentIds, int $version = null, array $translations = null): array
906
    {
907
        $queryBuilder = $this->connection->createQueryBuilder();
908
        $expr = $queryBuilder->expr();
909
        $queryBuilder
910
            ->select(
911
                'c.id AS ezcontentobject_id',
912
                'c.contentclass_id AS ezcontentobject_contentclass_id',
913
                'c.section_id AS ezcontentobject_section_id',
914
                'c.owner_id AS ezcontentobject_owner_id',
915
                'c.remote_id AS ezcontentobject_remote_id',
916
                'c.current_version AS ezcontentobject_current_version',
917
                'c.initial_language_id AS ezcontentobject_initial_language_id',
918
                'c.modified AS ezcontentobject_modified',
919
                'c.published AS ezcontentobject_published',
920
                'c.status AS ezcontentobject_status',
921
                'c.name AS ezcontentobject_name',
922
                'c.language_mask AS ezcontentobject_language_mask',
923
                'c.is_hidden AS ezcontentobject_is_hidden',
924
                'v.id AS ezcontentobject_version_id',
925
                'v.version AS ezcontentobject_version_version',
926
                'v.modified AS ezcontentobject_version_modified',
927
                'v.creator_id AS ezcontentobject_version_creator_id',
928
                'v.created AS ezcontentobject_version_created',
929
                'v.status AS ezcontentobject_version_status',
930
                'v.language_mask AS ezcontentobject_version_language_mask',
931
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
932
                'a.id AS ezcontentobject_attribute_id',
933
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
934
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
935
                'a.language_code AS ezcontentobject_attribute_language_code',
936
                'a.language_id AS ezcontentobject_attribute_language_id',
937
                'a.data_float AS ezcontentobject_attribute_data_float',
938
                'a.data_int AS ezcontentobject_attribute_data_int',
939
                'a.data_text AS ezcontentobject_attribute_data_text',
940
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
941
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
942
                't.main_node_id AS ezcontentobject_tree_main_node_id'
943
            )
944
            ->from('ezcontentobject', 'c')
945
            ->innerJoin(
946
                'c',
947
                'ezcontentobject_version',
948
                'v',
949
                $expr->andX(
950
                    $expr->eq('c.id', 'v.contentobject_id'),
951
                    $expr->eq('v.version', $version ?? 'c.current_version')
952
                )
953
            )
954
            ->innerJoin(
955
                'v',
956
                'ezcontentobject_attribute',
957
                'a',
958
                $expr->andX(
959
                    $expr->eq('v.contentobject_id', 'a.contentobject_id'),
960
                    $expr->eq('v.version', 'a.version')
961
                )
962
            )
963
            ->leftJoin(
964
                'c',
965
                'ezcontentobject_tree',
966
                't',
967
                $expr->andX(
968
                    $expr->eq('c.id', 't.contentobject_id'),
969
                    $expr->eq('t.node_id', 't.main_node_id')
970
                )
971
            );
972
973
        $queryBuilder->where(
974
            $expr->in(
975
                'c.id',
976
                $queryBuilder->createNamedParameter($contentIds, Connection::PARAM_INT_ARRAY)
977
            )
978
        );
979
980
        if (!empty($translations)) {
981
            $queryBuilder->andWhere(
982
                $expr->in(
983
                    'a.language_code',
984
                    $queryBuilder->createNamedParameter($translations, Connection::PARAM_STR_ARRAY)
985
                )
986
            );
987
        }
988
989
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
990
    }
991
992
    /**
993
     * Get query builder to load Content Info data.
994
     *
995
     * @see loadContentInfo(), loadContentInfoByRemoteId(), loadContentInfoList(), loadContentInfoByLocationId()
996
     *
997
     * @param bool $joinMainLocation
998
     *
999
     * @return \Doctrine\DBAL\Query\QueryBuilder
1000
     */
1001
    private function createLoadContentInfoQueryBuilder(bool $joinMainLocation = true): DoctrineQueryBuilder
1002
    {
1003
        $queryBuilder = $this->connection->createQueryBuilder();
1004
        $expr = $queryBuilder->expr();
1005
1006
        $joinCondition = $expr->eq('c.id', 't.contentobject_id');
1007
        if ($joinMainLocation) {
1008
            // wrap join condition with AND operator and join by a Main Location
1009
            $joinCondition = $expr->andX(
1010
                $joinCondition,
1011
                $expr->eq('t.node_id', 't.main_node_id')
1012
            );
1013
        }
1014
1015
        $queryBuilder
1016
            ->select('c.*', 't.main_node_id AS ezcontentobject_tree_main_node_id')
1017
            ->from('ezcontentobject', 'c')
1018
            ->leftJoin(
1019
                'c',
1020
                'ezcontentobject_tree',
1021
                't',
1022
                $joinCondition
0 ignored issues
show
Bug introduced by
It seems like $joinCondition defined by $expr->andX($joinConditi...id', 't.main_node_id')) on line 1009 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...
1023
            );
1024
1025
        return $queryBuilder;
1026
    }
1027
1028
    /**
1029
     * Loads info for content identified by $contentId.
1030
     * Will basically return a hash containing all field values for ezcontentobject table plus some additional keys:
1031
     *  - always_available => Boolean indicating if content's language mask contains alwaysAvailable bit field
1032
     *  - main_language_code => Language code for main (initial) language. E.g. "eng-GB".
1033
     *
1034
     * @param int $contentId
1035
     *
1036
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1037
     *
1038
     * @return array
1039
     */
1040 View Code Duplication
    public function loadContentInfo($contentId)
1041
    {
1042
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1043
        $queryBuilder
1044
            ->where('c.id = :id')
1045
            ->setParameter('id', $contentId, ParameterType::INTEGER);
1046
1047
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1048
        if (empty($results)) {
1049
            throw new NotFound('content', "id: $contentId");
1050
        }
1051
1052
        return $results[0];
1053
    }
1054
1055
    public function loadContentInfoList(array $contentIds)
1056
    {
1057
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1058
        $queryBuilder
1059
            ->where('c.id IN (:ids)')
1060
            ->setParameter('ids', $contentIds, Connection::PARAM_INT_ARRAY);
1061
1062
        return $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1063
    }
1064
1065
    /**
1066
     * Loads info for a content object identified by its remote ID.
1067
     *
1068
     * Returns an array with the relevant data.
1069
     *
1070
     * @param mixed $remoteId
1071
     *
1072
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1073
     *
1074
     * @return array
1075
     */
1076 View Code Duplication
    public function loadContentInfoByRemoteId($remoteId)
1077
    {
1078
        $queryBuilder = $this->createLoadContentInfoQueryBuilder();
1079
        $queryBuilder
1080
            ->where('c.remote_id = :id')
1081
            ->setParameter('id', $remoteId, ParameterType::STRING);
1082
1083
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1084
        if (empty($results)) {
1085
            throw new NotFound('content', "remote_id: $remoteId");
1086
        }
1087
1088
        return $results[0];
1089
    }
1090
1091
    /**
1092
     * Loads info for a content object identified by its location ID (node ID).
1093
     *
1094
     * Returns an array with the relevant data.
1095
     *
1096
     * @param int $locationId
1097
     *
1098
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1099
     *
1100
     * @return array
1101
     */
1102 View Code Duplication
    public function loadContentInfoByLocationId($locationId)
1103
    {
1104
        $queryBuilder = $this->createLoadContentInfoQueryBuilder(false);
1105
        $queryBuilder
1106
            ->where('t.node_id = :id')
1107
            ->setParameter('id', $locationId, ParameterType::INTEGER);
1108
1109
        $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE);
1110
        if (empty($results)) {
1111
            throw new NotFound('content', "node_id: $locationId");
1112
        }
1113
1114
        return $results[0];
1115
    }
1116
1117
    /**
1118
     * Loads version info for content identified by $contentId and $versionNo.
1119
     * Will basically return a hash containing all field values from ezcontentobject_version table plus following keys:
1120
     *  - names => Hash of content object names. Key is the language code, value is the name.
1121
     *  - 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.
1122
     *  - initial_language_code => Language code for initial language in this version.
1123
     *
1124
     * @param int $contentId
1125
     * @param int $versionNo
1126
     *
1127
     * @return array
1128
     */
1129 View Code Duplication
    public function loadVersionInfo($contentId, $versionNo)
1130
    {
1131
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1132
        $query->where(
1133
            $query->expr->lAnd(
1134
                $query->expr->eq(
1135
                    $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1136
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1137
                ),
1138
                $query->expr->eq(
1139
                    $this->dbHandler->quoteColumn('version', 'ezcontentobject_version'),
1140
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1141
                )
1142
            )
1143
        );
1144
        $statement = $query->prepare();
1145
        $statement->execute();
1146
1147
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1148
    }
1149
1150
    /**
1151
     * Returns data for all versions with given status created by the given $userId.
1152
     *
1153
     * @param int $userId
1154
     * @param int $status
1155
     *
1156
     * @return string[][]
1157
     */
1158
    public function listVersionsForUser($userId, $status = VersionInfo::STATUS_DRAFT)
1159
    {
1160
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1161
        $query->where(
1162
            $query->expr->lAnd(
1163
                $query->expr->eq(
1164
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1165
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1166
                ),
1167
                $query->expr->eq(
1168
                    $this->dbHandler->quoteColumn('creator_id', 'ezcontentobject_version'),
1169
                    $query->bindValue($userId, null, \PDO::PARAM_INT)
1170
                )
1171
            )
1172
        );
1173
1174
        return $this->listVersionsHelper($query);
1175
    }
1176
1177
    /**
1178
     * Returns all version data for the given $contentId, optionally filtered by status.
1179
     *
1180
     * Result is returned with oldest version first (using version id as it has index and is auto increment).
1181
     *
1182
     * @param mixed $contentId
1183
     * @param mixed|null $status Optional argument to filter versions by status, like {@see VersionInfo::STATUS_ARCHIVED}.
1184
     * @param int $limit Limit for items returned, -1 means none.
1185
     *
1186
     * @return string[][]
1187
     */
1188
    public function listVersions($contentId, $status = null, $limit = -1)
1189
    {
1190
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1191
1192
        $filter = $query->expr->eq(
1193
            $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1194
            $query->bindValue($contentId, null, \PDO::PARAM_INT)
1195
        );
1196
1197
        if ($status !== null) {
1198
            $filter = $query->expr->lAnd(
1199
                $filter,
1200
                $query->expr->eq(
1201
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1202
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1203
                )
1204
            );
1205
        }
1206
1207
        $query->where($filter);
1208
1209
        if ($limit > 0) {
1210
            $query->limit($limit);
1211
        }
1212
1213
        return $this->listVersionsHelper($query);
1214
    }
1215
1216
    /**
1217
     * Helper for {@see listVersions()} and {@see listVersionsForUser()} that filters duplicates
1218
     * that are the result of the cartesian product performed by createVersionInfoFindQuery().
1219
     *
1220
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
1221
     *
1222
     * @return string[][]
1223
     */
1224
    private function listVersionsHelper(SelectQuery $query)
1225
    {
1226
        $query->orderBy(
1227
            $this->dbHandler->quoteColumn('id', 'ezcontentobject_version')
1228
        );
1229
1230
        $statement = $query->prepare();
1231
        $statement->execute();
1232
1233
        $results = [];
1234
        $previousId = null;
1235
        foreach ($statement->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1236
            if ($row['ezcontentobject_version_id'] == $previousId) {
1237
                continue;
1238
            }
1239
1240
            $previousId = $row['ezcontentobject_version_id'];
1241
            $results[] = $row;
1242
        }
1243
1244
        return $results;
1245
    }
1246
1247
    /**
1248
     * Returns all version numbers for the given $contentId.
1249
     *
1250
     * @param mixed $contentId
1251
     *
1252
     * @return int[]
1253
     */
1254
    public function listVersionNumbers($contentId)
1255
    {
1256
        $query = $this->dbHandler->createSelectQuery();
1257
        $query->selectDistinct(
1258
            $this->dbHandler->quoteColumn('version')
1259
        )->from(
1260
            $this->dbHandler->quoteTable('ezcontentobject_version')
1261
        )->where(
1262
            $query->expr->eq(
1263
                $this->dbHandler->quoteColumn('contentobject_id'),
1264
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1265
            )
1266
        );
1267
1268
        $statement = $query->prepare();
1269
        $statement->execute();
1270
1271
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1272
    }
1273
1274
    /**
1275
     * Returns last version number for content identified by $contentId.
1276
     *
1277
     * @param int $contentId
1278
     *
1279
     * @return int
1280
     */
1281 View Code Duplication
    public function getLastVersionNumber($contentId)
1282
    {
1283
        $query = $this->dbHandler->createSelectQuery();
1284
        $query->select(
1285
            $query->expr->max($this->dbHandler->quoteColumn('version'))
1286
        )->from(
1287
            $this->dbHandler->quoteTable('ezcontentobject_version')
1288
        )->where(
1289
            $query->expr->eq(
1290
                $this->dbHandler->quoteColumn('contentobject_id'),
1291
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1292
            )
1293
        );
1294
1295
        $statement = $query->prepare();
1296
        $statement->execute();
1297
1298
        return (int)$statement->fetchColumn();
1299
    }
1300
1301
    /**
1302
     * Returns all IDs for locations that refer to $contentId.
1303
     *
1304
     * @param int $contentId
1305
     *
1306
     * @return int[]
1307
     */
1308
    public function getAllLocationIds($contentId)
1309
    {
1310
        $query = $this->dbHandler->createSelectQuery();
1311
        $query->select(
1312
            $this->dbHandler->quoteColumn('node_id')
1313
        )->from(
1314
            $this->dbHandler->quoteTable('ezcontentobject_tree')
1315
        )->where(
1316
            $query->expr->eq(
1317
                $this->dbHandler->quoteColumn('contentobject_id'),
1318
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1319
            )
1320
        );
1321
1322
        $statement = $query->prepare();
1323
        $statement->execute();
1324
1325
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1326
    }
1327
1328
    /**
1329
     * Returns all field IDs of $contentId grouped by their type.
1330
     * If $versionNo is set only field IDs for that version are returned.
1331
     * If $languageCode is set, only field IDs for that language are returned.
1332
     *
1333
     * @param int $contentId
1334
     * @param int|null $versionNo
1335
     * @param string|null $languageCode
1336
     *
1337
     * @return int[][]
1338
     */
1339
    public function getFieldIdsByType($contentId, $versionNo = null, $languageCode = null)
1340
    {
1341
        $query = $this->dbHandler->createSelectQuery();
1342
        $query->select(
1343
            $this->dbHandler->quoteColumn('id'),
1344
            $this->dbHandler->quoteColumn('data_type_string')
1345
        )->from(
1346
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1347
        )->where(
1348
            $query->expr->eq(
1349
                $this->dbHandler->quoteColumn('contentobject_id'),
1350
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1351
            )
1352
        );
1353
1354
        if (isset($versionNo)) {
1355
            $query->where(
1356
                $query->expr->eq(
1357
                    $this->dbHandler->quoteColumn('version'),
1358
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1359
                )
1360
            );
1361
        }
1362
1363
        if (isset($languageCode)) {
1364
            $query->where(
1365
                $query->expr->eq(
1366
                    $this->dbHandler->quoteColumn('language_code'),
1367
                    $query->bindValue($languageCode, null, \PDO::PARAM_STR)
1368
                )
1369
            );
1370
        }
1371
1372
        $statement = $query->prepare();
1373
        $statement->execute();
1374
1375
        $result = [];
1376
        foreach ($statement->fetchAll() as $row) {
1377
            if (!isset($result[$row['data_type_string']])) {
1378
                $result[$row['data_type_string']] = [];
1379
            }
1380
            $result[$row['data_type_string']][] = (int)$row['id'];
1381
        }
1382
1383
        return $result;
1384
    }
1385
1386
    /**
1387
     * Deletes relations to and from $contentId.
1388
     * If $versionNo is set only relations for that version are deleted.
1389
     *
1390
     * @param int $contentId
1391
     * @param int|null $versionNo
1392
     */
1393
    public function deleteRelations($contentId, $versionNo = null)
1394
    {
1395
        $query = $this->dbHandler->createDeleteQuery();
1396
        $query->deleteFrom(
1397
            $this->dbHandler->quoteTable('ezcontentobject_link')
1398
        );
1399
1400
        if (isset($versionNo)) {
1401
            $query->where(
1402
                $query->expr->lAnd(
1403
                    $query->expr->eq(
1404
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1405
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1406
                    ),
1407
                    $query->expr->eq(
1408
                        $this->dbHandler->quoteColumn('from_contentobject_version'),
1409
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1410
                    )
1411
                )
1412
            );
1413
        } else {
1414
            $query->where(
1415
                $query->expr->lOr(
1416
                    $query->expr->eq(
1417
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1418
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1419
                    ),
1420
                    $query->expr->eq(
1421
                        $this->dbHandler->quoteColumn('to_contentobject_id'),
1422
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1423
                    )
1424
                )
1425
            );
1426
        }
1427
1428
        $query->prepare()->execute();
1429
    }
1430
1431
    /**
1432
     * Removes relations to Content with $contentId from Relation and RelationList field type fields.
1433
     *
1434
     * @param int $contentId
1435
     */
1436
    public function removeReverseFieldRelations($contentId)
1437
    {
1438
        $query = $this->dbHandler->createSelectQuery();
1439
        $query
1440
            ->select('ezcontentobject_attribute.*')
1441
            ->from('ezcontentobject_attribute')
1442
            ->innerJoin(
1443
                'ezcontentobject_link',
1444
                $query->expr->lAnd(
1445
                    $query->expr->eq(
1446
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1447
                        $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_attribute')
1448
                    ),
1449
                    $query->expr->eq(
1450
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1451
                        $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute')
1452
                    ),
1453
                    $query->expr->eq(
1454
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_link'),
1455
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_attribute')
1456
                    )
1457
                )
1458
            )
1459
            ->where(
1460
                $query->expr->eq(
1461
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1462
                    $query->bindValue($contentId, null, PDO::PARAM_INT)
1463
                ),
1464
                $query->expr->gt(
1465
                    $query->expr->bitAnd(
1466
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1467
                        $query->bindValue(8, null, PDO::PARAM_INT)
1468
                    ),
1469
                    0
1470
                )
1471
            );
1472
1473
        $statement = $query->prepare();
1474
        $statement->execute();
1475
1476
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
1477
            if ($row['data_type_string'] === 'ezobjectrelation') {
1478
                $this->removeRelationFromRelationField($row);
1479
            }
1480
1481
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1482
                $this->removeRelationFromRelationListField($contentId, $row);
1483
            }
1484
        }
1485
    }
1486
1487
    /**
1488
     * Updates field value of RelationList field type identified by given $row data,
1489
     * removing relations toward given $contentId.
1490
     *
1491
     * @param int $contentId
1492
     * @param array $row
1493
     */
1494
    protected function removeRelationFromRelationListField($contentId, array $row)
1495
    {
1496
        $document = new DOMDocument('1.0', 'utf-8');
1497
        $document->loadXML($row['data_text']);
1498
1499
        $xpath = new DOMXPath($document);
1500
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1501
1502
        $relationItems = $xpath->query($xpathExpression);
1503
        foreach ($relationItems as $relationItem) {
1504
            $relationItem->parentNode->removeChild($relationItem);
1505
        }
1506
1507
        $query = $this->dbHandler->createUpdateQuery();
1508
        $query
1509
            ->update('ezcontentobject_attribute')
1510
            ->set(
1511
                'data_text',
1512
                $query->bindValue($document->saveXML(), null, PDO::PARAM_STR)
1513
            )
1514
            ->where(
1515
                $query->expr->lAnd(
1516
                    $query->expr->eq(
1517
                        $this->dbHandler->quoteColumn('id'),
1518
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1519
                    ),
1520
                    $query->expr->eq(
1521
                        $this->dbHandler->quoteColumn('version'),
1522
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1523
                    )
1524
                )
1525
            );
1526
1527
        $query->prepare()->execute();
1528
    }
1529
1530
    /**
1531
     * Updates field value of Relation field type identified by given $row data,
1532
     * removing relation data.
1533
     *
1534
     * @param array $row
1535
     */
1536
    protected function removeRelationFromRelationField(array $row)
1537
    {
1538
        $query = $this->dbHandler->createUpdateQuery();
1539
        $query
1540
            ->update('ezcontentobject_attribute')
1541
            ->set('data_int', $query->bindValue(null, null, PDO::PARAM_INT))
1542
            ->set('sort_key_int', $query->bindValue(0, null, PDO::PARAM_INT))
1543
            ->where(
1544
                $query->expr->lAnd(
1545
                    $query->expr->eq(
1546
                        $this->dbHandler->quoteColumn('id'),
1547
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1548
                    ),
1549
                    $query->expr->eq(
1550
                        $this->dbHandler->quoteColumn('version'),
1551
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1552
                    )
1553
                )
1554
            );
1555
1556
        $query->prepare()->execute();
1557
    }
1558
1559
    /**
1560
     * Deletes the field with the given $fieldId.
1561
     *
1562
     * @param int $fieldId
1563
     */
1564 View Code Duplication
    public function deleteField($fieldId)
1565
    {
1566
        $query = $this->dbHandler->createDeleteQuery();
1567
        $query->deleteFrom(
1568
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1569
        )->where(
1570
            $query->expr->eq(
1571
                $this->dbHandler->quoteColumn('id'),
1572
                $query->bindValue($fieldId, null, \PDO::PARAM_INT)
1573
            )
1574
        );
1575
1576
        $query->prepare()->execute();
1577
    }
1578
1579
    /**
1580
     * Deletes all fields of $contentId in all versions.
1581
     * If $versionNo is set only fields for that version are deleted.
1582
     *
1583
     * @param int $contentId
1584
     * @param int|null $versionNo
1585
     */
1586
    public function deleteFields($contentId, $versionNo = null)
1587
    {
1588
        $query = $this->dbHandler->createDeleteQuery();
1589
        $query->deleteFrom('ezcontentobject_attribute')
1590
            ->where(
1591
                $query->expr->eq(
1592
                    $this->dbHandler->quoteColumn('contentobject_id'),
1593
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1594
                )
1595
            );
1596
1597
        if (isset($versionNo)) {
1598
            $query->where(
1599
                $query->expr->eq(
1600
                    $this->dbHandler->quoteColumn('version'),
1601
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1602
                )
1603
            );
1604
        }
1605
1606
        $query->prepare()->execute();
1607
    }
1608
1609
    /**
1610
     * Deletes all versions of $contentId.
1611
     * If $versionNo is set only that version is deleted.
1612
     *
1613
     * @param int $contentId
1614
     * @param int|null $versionNo
1615
     */
1616
    public function deleteVersions($contentId, $versionNo = null)
1617
    {
1618
        $query = $this->dbHandler->createDeleteQuery();
1619
        $query->deleteFrom('ezcontentobject_version')
1620
            ->where(
1621
                $query->expr->eq(
1622
                    $this->dbHandler->quoteColumn('contentobject_id'),
1623
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1624
                )
1625
            );
1626
1627
        if (isset($versionNo)) {
1628
            $query->where(
1629
                $query->expr->eq(
1630
                    $this->dbHandler->quoteColumn('version'),
1631
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1632
                )
1633
            );
1634
        }
1635
1636
        $query->prepare()->execute();
1637
    }
1638
1639
    /**
1640
     * Deletes all names of $contentId.
1641
     * If $versionNo is set only names for that version are deleted.
1642
     *
1643
     * @param int $contentId
1644
     * @param int|null $versionNo
1645
     */
1646
    public function deleteNames($contentId, $versionNo = null)
1647
    {
1648
        $query = $this->dbHandler->createDeleteQuery();
1649
        $query->deleteFrom('ezcontentobject_name')
1650
            ->where(
1651
                $query->expr->eq(
1652
                    $this->dbHandler->quoteColumn('contentobject_id'),
1653
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1654
                )
1655
            );
1656
1657
        if (isset($versionNo)) {
1658
            $query->where(
1659
                $query->expr->eq(
1660
                    $this->dbHandler->quoteColumn('content_version'),
1661
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1662
                )
1663
            );
1664
        }
1665
1666
        $query->prepare()->execute();
1667
    }
1668
1669
    /**
1670
     * Sets the name for Content $contentId in version $version to $name in $language.
1671
     *
1672
     * @param int $contentId
1673
     * @param int $version
1674
     * @param string $name
1675
     * @param string $language
1676
     */
1677
    public function setName($contentId, $version, $name, $language)
1678
    {
1679
        $language = $this->languageHandler->loadByLanguageCode($language);
1680
1681
        // Is it an insert or an update ?
1682
        $qSelect = $this->dbHandler->createSelectQuery();
1683
        $qSelect
1684
            ->select(
1685
                $qSelect->alias($qSelect->expr->count('*'), 'count')
1686
            )
1687
            ->from($this->dbHandler->quoteTable('ezcontentobject_name'))
1688
            ->where(
1689
                $qSelect->expr->lAnd(
1690
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $qSelect->bindValue($contentId)),
1691
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_version'), $qSelect->bindValue($version)),
1692
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_translation'), $qSelect->bindValue($language->languageCode))
1693
                )
1694
            );
1695
        $stmt = $qSelect->prepare();
1696
        $stmt->execute();
1697
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1698
1699
        $insert = $res[0]['count'] == 0;
1700
        if ($insert) {
1701
            $q = $this->dbHandler->createInsertQuery();
1702
            $q->insertInto($this->dbHandler->quoteTable('ezcontentobject_name'));
1703
        } else {
1704
            $q = $this->dbHandler->createUpdateQuery();
1705
            $q->update($this->dbHandler->quoteTable('ezcontentobject_name'))
1706
                ->where(
1707
                    $q->expr->lAnd(
1708
                        $q->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $q->bindValue($contentId)),
1709
                        $q->expr->eq($this->dbHandler->quoteColumn('content_version'), $q->bindValue($version)),
1710
                        $q->expr->eq($this->dbHandler->quoteColumn('content_translation'), $q->bindValue($language->languageCode))
1711
                    )
1712
                );
1713
        }
1714
1715
        $q->set(
1716
            $this->dbHandler->quoteColumn('contentobject_id'),
1717
            $q->bindValue($contentId, null, \PDO::PARAM_INT)
1718
        )->set(
1719
            $this->dbHandler->quoteColumn('content_version'),
1720
            $q->bindValue($version, null, \PDO::PARAM_INT)
1721
        )->set(
1722
            $this->dbHandler->quoteColumn('language_id'),
1723
            '(' . $this->getLanguageQuery()->getQuery() . ')'
1724
        )->set(
1725
            $this->dbHandler->quoteColumn('content_translation'),
1726
            $q->bindValue($language->languageCode)
1727
        )->set(
1728
            $this->dbHandler->quoteColumn('real_translation'),
1729
            $q->bindValue($language->languageCode)
1730
        )->set(
1731
            $this->dbHandler->quoteColumn('name'),
1732
            $q->bindValue($name)
1733
        );
1734
        $q->bindValue($language->id, ':languageId', \PDO::PARAM_INT);
1735
        $q->bindValue($contentId, ':contentId', \PDO::PARAM_INT);
1736
        $q->prepare()->execute();
1737
    }
1738
1739
    /**
1740
     * Returns a language sub select query for setName.
1741
     *
1742
     * Return sub select query which gets proper language mask for alwaysAvailable Content.
1743
     *
1744
     * @return \eZ\Publish\Core\Persistence\Database\SelectQuery
1745
     */
1746
    private function getLanguageQuery()
1747
    {
1748
        $languageQuery = $this->dbHandler->createSelectQuery();
1749
        $languageQuery
1750
            ->select(
1751
                $languageQuery->expr->searchedCase(
1752
                    [
1753
                        $languageQuery->expr->lAnd(
1754
                            $languageQuery->expr->eq(
1755
                                $this->dbHandler->quoteColumn('initial_language_id'),
1756
                                ':languageId'
1757
                            ),
1758
                            // wrap bitwise check into another "neq" to provide cross-DBMS compatibility
1759
                            $languageQuery->expr->neq(
1760
                                $languageQuery->expr->bitAnd(
1761
                                    $this->dbHandler->quoteColumn('language_mask'),
1762
                                    ':languageId'
1763
                                ),
1764
                                0
1765
                            )
1766
                        ),
1767
                        $languageQuery->expr->bitOr(
1768
                            ':languageId',
1769
                            1
1770
                        ),
1771
                    ],
1772
                    ':languageId'
1773
                )
1774
            )
1775
            ->from('ezcontentobject')
1776
            ->where(
1777
                $languageQuery->expr->eq(
1778
                    'id',
1779
                    ':contentId'
1780
                )
1781
            );
1782
1783
        return $languageQuery;
1784
    }
1785
1786
    /**
1787
     * Deletes the actual content object referred to by $contentId.
1788
     *
1789
     * @param int $contentId
1790
     */
1791
    public function deleteContent($contentId)
1792
    {
1793
        $query = $this->dbHandler->createDeleteQuery();
1794
        $query->deleteFrom('ezcontentobject')
1795
            ->where(
1796
                $query->expr->eq(
1797
                    $this->dbHandler->quoteColumn('id'),
1798
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1799
                )
1800
            );
1801
1802
        $query->prepare()->execute();
1803
    }
1804
1805
    /**
1806
     * Loads relations from $contentId to published content, optionally only from $contentVersionNo.
1807
     *
1808
     * $relationType can also be filtered.
1809
     *
1810
     * @param int $contentId
1811
     * @param int $contentVersionNo
1812
     * @param int $relationType
1813
     *
1814
     * @return string[][] array of relation data
1815
     */
1816
    public function loadRelations($contentId, $contentVersionNo = null, $relationType = null)
1817
    {
1818
        $query = $this->queryBuilder->createRelationFindQuery();
1819
        $query->innerJoin(
1820
            $query->alias(
1821
                $this->dbHandler->quoteTable('ezcontentobject'),
1822
                'ezcontentobject_to'
1823
            ),
1824
            $query->expr->lAnd(
1825
                $query->expr->eq(
1826
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1827
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject_to')
1828
                ),
1829
                $query->expr->eq(
1830
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_to'),
1831
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1832
                )
1833
            )
1834
        )->where(
1835
            $query->expr->eq(
1836
                $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1837
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1838
            )
1839
        );
1840
1841
        // source version number
1842
        if (isset($contentVersionNo)) {
1843
            $query->where(
1844
                $query->expr->eq(
1845
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1846
                    $query->bindValue($contentVersionNo, null, \PDO::PARAM_INT)
1847
                )
1848
            );
1849
        } else { // from published version only
1850
            $query->from(
1851
                $this->dbHandler->quoteTable('ezcontentobject')
1852
            )->where(
1853
                $query->expr->lAnd(
1854
                    $query->expr->eq(
1855
                        $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1856
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1857
                    ),
1858
                    $query->expr->eq(
1859
                        $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1860
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1861
                    )
1862
                )
1863
            );
1864
        }
1865
1866
        // relation type
1867 View Code Duplication
        if (isset($relationType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1868
            $query->where(
1869
                $query->expr->gt(
1870
                    $query->expr->bitAnd(
1871
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1872
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1873
                    ),
1874
                    0
1875
                )
1876
            );
1877
        }
1878
1879
        $statement = $query->prepare();
1880
        $statement->execute();
1881
1882
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1883
    }
1884
1885
    /**
1886
     * Loads data that related to $toContentId.
1887
     *
1888
     * @param int $toContentId
1889
     * @param int $relationType
1890
     *
1891
     * @return mixed[][] Content data, array structured like {@see \eZ\Publish\Core\Persistence\Legacy\Content\Gateway::load()}
1892
     */
1893
    public function loadReverseRelations($toContentId, $relationType = null)
1894
    {
1895
        $query = $this->queryBuilder->createRelationFindQuery();
1896
        $query->where(
1897
            $query->expr->eq(
1898
                $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1899
                $query->bindValue($toContentId, null, \PDO::PARAM_INT)
1900
            )
1901
        );
1902
1903
        // ezcontentobject join
1904
        $query->from(
1905
            $this->dbHandler->quoteTable('ezcontentobject')
1906
        )->where(
1907
            $query->expr->lAnd(
1908
                $query->expr->eq(
1909
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1910
                    $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1911
                ),
1912
                $query->expr->eq(
1913
                    $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1914
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1915
                ),
1916
                $query->expr->eq(
1917
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject'),
1918
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1919
                )
1920
            )
1921
        );
1922
1923
        // relation type
1924 View Code Duplication
        if (isset($relationType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1925
            $query->where(
1926
                $query->expr->gt(
1927
                    $query->expr->bitAnd(
1928
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1929
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1930
                    ),
1931
                    0
1932
                )
1933
            );
1934
        }
1935
1936
        $statement = $query->prepare();
1937
1938
        $statement->execute();
1939
1940
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1941
    }
1942
1943
    /**
1944
     * Inserts a new relation database record.
1945
     *
1946
     * @param \eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct $createStruct
1947
     *
1948
     * @return int ID the inserted ID
1949
     */
1950
    public function insertRelation(RelationCreateStruct $createStruct)
1951
    {
1952
        $q = $this->dbHandler->createInsertQuery();
1953
        $q->insertInto(
1954
            $this->dbHandler->quoteTable('ezcontentobject_link')
1955
        )->set(
1956
            $this->dbHandler->quoteColumn('id'),
1957
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_link', 'id')
1958
        )->set(
1959
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
1960
            $q->bindValue((int)$createStruct->sourceFieldDefinitionId, null, \PDO::PARAM_INT)
1961
        )->set(
1962
            $this->dbHandler->quoteColumn('from_contentobject_id'),
1963
            $q->bindValue($createStruct->sourceContentId, null, \PDO::PARAM_INT)
1964
        )->set(
1965
            $this->dbHandler->quoteColumn('from_contentobject_version'),
1966
            $q->bindValue($createStruct->sourceContentVersionNo, null, \PDO::PARAM_INT)
1967
        )->set(
1968
            $this->dbHandler->quoteColumn('relation_type'),
1969
            $q->bindValue($createStruct->type, null, \PDO::PARAM_INT)
1970
        )->set(
1971
            $this->dbHandler->quoteColumn('to_contentobject_id'),
1972
            $q->bindValue($createStruct->destinationContentId, null, \PDO::PARAM_INT)
1973
        );
1974
1975
        $q->prepare()->execute();
1976
1977
        return $this->dbHandler->lastInsertId(
1978
            $this->dbHandler->getSequenceName('ezcontentobject_link', 'id')
1979
        );
1980
    }
1981
1982
    /**
1983
     * Deletes the relation with the given $relationId.
1984
     *
1985
     * @param int $relationId
1986
     * @param int $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
1987
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
1988
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
1989
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
1990
     */
1991
    public function deleteRelation($relationId, $type)
1992
    {
1993
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
1994
        // existing relation type by given $relationId for comparison
1995
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
1996
        $query = $this->dbHandler->createSelectQuery();
1997
        $query->select(
1998
            $this->dbHandler->quoteColumn('relation_type')
1999
        )->from(
2000
            $this->dbHandler->quoteTable('ezcontentobject_link')
2001
        )->where(
2002
            $query->expr->eq(
2003
                $this->dbHandler->quoteColumn('id'),
2004
                $query->bindValue($relationId, null, \PDO::PARAM_INT)
2005
            )
2006
        );
2007
2008
        $statement = $query->prepare();
2009
        $statement->execute();
2010
        $loadedRelationType = $statement->fetchColumn();
2011
2012
        if (!$loadedRelationType) {
2013
            return;
2014
        }
2015
2016
        // If relation type matches then delete
2017
        if ($loadedRelationType == $type) {
2018
            /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
2019
            $query = $this->dbHandler->createDeleteQuery();
2020
            $query->deleteFrom(
2021
                'ezcontentobject_link'
2022
            )->where(
2023
                $query->expr->eq(
2024
                    $this->dbHandler->quoteColumn('id'),
2025
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2026
                )
2027
            );
2028
2029
            $query->prepare()->execute();
2030
        } elseif ($loadedRelationType & $type) { // If relation type is composite update bitmask
2031
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
2032
            $query = $this->dbHandler->createUpdateQuery();
2033
            $query->update(
2034
                $this->dbHandler->quoteTable('ezcontentobject_link')
2035
            )->set(
2036
                $this->dbHandler->quoteColumn('relation_type'),
2037
                $query->expr->bitAnd(
2038
                    $this->dbHandler->quoteColumn('relation_type'),
2039
                    $query->bindValue(~$type, null, \PDO::PARAM_INT)
2040
                )
2041
            )->where(
2042
                $query->expr->eq(
2043
                    $this->dbHandler->quoteColumn('id'),
2044
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
2045
                )
2046
            );
2047
2048
            $query->prepare()->execute();
2049
        } else {
2050
            // No match, do nothing
2051
        }
2052
    }
2053
2054
    /**
2055
     * Returns all Content IDs for a given $contentTypeId.
2056
     *
2057
     * @param int $contentTypeId
2058
     *
2059
     * @return int[]
2060
     */
2061
    public function getContentIdsByContentTypeId($contentTypeId)
2062
    {
2063
        $query = $this->dbHandler->createSelectQuery();
2064
        $query
2065
            ->select($this->dbHandler->quoteColumn('id'))
2066
            ->from($this->dbHandler->quoteTable('ezcontentobject'))
2067
            ->where(
2068
                $query->expr->eq(
2069
                    $this->dbHandler->quoteColumn('contentclass_id'),
2070
                    $query->bindValue($contentTypeId, null, PDO::PARAM_INT)
2071
                )
2072
            );
2073
2074
        $statement = $query->prepare();
2075
        $statement->execute();
2076
2077
        return $statement->fetchAll(PDO::FETCH_COLUMN);
2078
    }
2079
2080
    /**
2081
     * Load name data for set of content id's and corresponding version number.
2082
     *
2083
     * @param array[] $rows array of hashes with 'id' and 'version' to load names for
2084
     *
2085
     * @return array
2086
     */
2087
    public function loadVersionedNameData($rows)
2088
    {
2089
        $query = $this->queryBuilder->createNamesQuery();
2090
        $conditions = [];
2091
        foreach ($rows as $row) {
2092
            $conditions[] = $query->expr->lAnd(
2093
                $query->expr->eq(
2094
                    $this->dbHandler->quoteColumn('contentobject_id'),
2095
                    $query->bindValue($row['id'], null, \PDO::PARAM_INT)
2096
                ),
2097
                $query->expr->eq(
2098
                    $this->dbHandler->quoteColumn('content_version'),
2099
                    $query->bindValue($row['version'], null, \PDO::PARAM_INT)
2100
                )
2101
            );
2102
        }
2103
2104
        $query->where($query->expr->lOr($conditions));
2105
        $stmt = $query->prepare();
2106
        $stmt->execute();
2107
2108
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
2109
    }
2110
2111
    /**
2112
     * Batch method for copying all relation meta data for copied Content object.
2113
     *
2114
     * {@inheritdoc}
2115
     *
2116
     * @param int $originalContentId
2117
     * @param int $copiedContentId
2118
     * @param int|null $versionNo If specified only copy for a given version number, otherwise all.
2119
     */
2120
    public function copyRelations($originalContentId, $copiedContentId, $versionNo = null)
2121
    {
2122
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
2123
        $sql = 'INSERT INTO ezcontentobject_link ( contentclassattribute_id, from_contentobject_id, from_contentobject_version, relation_type, to_contentobject_id )
2124
                SELECT  L2.contentclassattribute_id, :copied_id, L2.from_contentobject_version, L2.relation_type, L2.to_contentobject_id
2125
                FROM    ezcontentobject_link AS L2
2126
                WHERE   L2.from_contentobject_id = :original_id';
2127
2128
        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...
2129
            $stmt = $this->connection->prepare($sql . ' AND L2.from_contentobject_version = :version');
2130
            $stmt->bindValue('version', $versionNo, PDO::PARAM_INT);
2131
        } else {
2132
            $stmt = $this->connection->prepare($sql);
2133
        }
2134
2135
        $stmt->bindValue('original_id', $originalContentId, PDO::PARAM_INT);
2136
        $stmt->bindValue('copied_id', $copiedContentId, PDO::PARAM_INT);
2137
2138
        $stmt->execute();
2139
    }
2140
2141
    /**
2142
     * Remove the specified translation from the Content Object Version.
2143
     *
2144
     * @param int $contentId
2145
     * @param string $languageCode language code of the translation
2146
     * @throws \Doctrine\DBAL\DBALException
2147
     */
2148 View Code Duplication
    public function deleteTranslationFromContent($contentId, $languageCode)
2149
    {
2150
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2151
2152
        $this->connection->beginTransaction();
2153
        try {
2154
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
2155
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
2156
            $this->deleteTranslationFromContentObject($contentId, $language->id);
2157
2158
            $this->connection->commit();
2159
        } catch (DBALException $e) {
2160
            $this->connection->rollBack();
2161
            throw $e;
2162
        }
2163
    }
2164
2165
    /**
2166
     * Delete Content fields (attributes) for the given Translation.
2167
     * If $versionNo is given, fields for that Version only will be deleted.
2168
     *
2169
     * @param string $languageCode
2170
     * @param int $contentId
2171
     * @param int $versionNo (optional) filter by versionNo
2172
     */
2173 View Code Duplication
    public function deleteTranslatedFields($languageCode, $contentId, $versionNo = null)
2174
    {
2175
        $query = $this->connection->createQueryBuilder();
2176
        $query
2177
            ->delete('ezcontentobject_attribute')
2178
            ->where('contentobject_id = :contentId')
2179
            ->andWhere('language_code = :languageCode')
2180
            ->setParameters(
2181
                [
2182
                    ':contentId' => $contentId,
2183
                    ':languageCode' => $languageCode,
2184
                ]
2185
            )
2186
        ;
2187
2188
        if (null !== $versionNo) {
2189
            $query
2190
                ->andWhere('version = :versionNo')
2191
                ->setParameter(':versionNo', $versionNo)
2192
            ;
2193
        }
2194
2195
        $query->execute();
2196
    }
2197
2198
    /**
2199
     * Delete the specified Translation from the given Version.
2200
     *
2201
     * @param int $contentId
2202
     * @param int $versionNo
2203
     * @param string $languageCode
2204
     * @throws \Doctrine\DBAL\DBALException
2205
     */
2206 View Code Duplication
    public function deleteTranslationFromVersion($contentId, $versionNo, $languageCode)
2207
    {
2208
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2209
2210
        $this->connection->beginTransaction();
2211
        try {
2212
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
2213
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
2214
2215
            $this->connection->commit();
2216
        } catch (DBALException $e) {
2217
            $this->connection->rollBack();
2218
            throw $e;
2219
        }
2220
    }
2221
2222
    /**
2223
     * Delete translation from the ezcontentobject_name table.
2224
     *
2225
     * @param int $contentId
2226
     * @param string $languageCode
2227
     * @param int $versionNo optional, if specified, apply to this Version only.
2228
     */
2229 View Code Duplication
    private function deleteTranslationFromContentNames($contentId, $languageCode, $versionNo = null)
2230
    {
2231
        $query = $this->connection->createQueryBuilder();
2232
        $query
2233
            ->delete('ezcontentobject_name')
2234
            ->where('contentobject_id=:contentId')
2235
            ->andWhere('real_translation=:languageCode')
2236
            ->setParameters(
2237
                [
2238
                    ':languageCode' => $languageCode,
2239
                    ':contentId' => $contentId,
2240
                ]
2241
            )
2242
        ;
2243
2244
        if (null !== $versionNo) {
2245
            $query
2246
                ->andWhere('content_version = :versionNo')
2247
                ->setParameter(':versionNo', $versionNo)
2248
            ;
2249
        }
2250
2251
        $query->execute();
2252
    }
2253
2254
    /**
2255
     * Remove language from language_mask of ezcontentobject.
2256
     *
2257
     * @param int $contentId
2258
     * @param int $languageId
2259
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2260
     */
2261
    private function deleteTranslationFromContentObject($contentId, $languageId)
2262
    {
2263
        $query = $this->connection->createQueryBuilder();
2264
        $query->update('ezcontentobject')
2265
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2266
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2267
            ->set('modified', ':now')
2268
            ->where('id = :contentId')
2269
            ->andWhere(
2270
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2271
                $query->expr()->andX(
2272
                    'language_mask & ~ ' . $languageId . ' <> 0',
2273
                    'language_mask & ~ ' . $languageId . ' <> 1'
2274
                )
2275
            )
2276
            ->setParameter(':now', time())
2277
            ->setParameter(':contentId', $contentId)
2278
        ;
2279
2280
        $rowCount = $query->execute();
2281
2282
        // no rows updated means that most likely somehow it was the last remaining translation
2283
        if ($rowCount === 0) {
2284
            throw new BadStateException(
2285
                '$languageCode',
2286
                'Specified translation is the only one Content Object Version has'
2287
            );
2288
        }
2289
    }
2290
2291
    /**
2292
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
2293
     * if it matches the removed one.
2294
     *
2295
     * @param int $contentId
2296
     * @param int $languageId
2297
     * @param int $versionNo optional, if specified, apply to this Version only.
2298
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2299
     */
2300
    private function deleteTranslationFromContentVersions($contentId, $languageId, $versionNo = null)
2301
    {
2302
        $query = $this->connection->createQueryBuilder();
2303
        $query->update('ezcontentobject_version')
2304
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2305
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2306
            ->set('modified', ':now')
2307
            // update initial_language_id only if it matches removed translation languageId
2308
            ->set(
2309
                'initial_language_id',
2310
                'CASE WHEN initial_language_id = :languageId ' .
2311
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
2312
                'ELSE initial_language_id END'
2313
            )
2314
            ->where('contentobject_id = :contentId')
2315
            ->andWhere(
2316
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2317
                $query->expr()->andX(
2318
                    'language_mask & ~ ' . $languageId . ' <> 0',
2319
                    'language_mask & ~ ' . $languageId . ' <> 1'
2320
                )
2321
            )
2322
            ->setParameter(':now', time())
2323
            ->setParameter(':contentId', $contentId)
2324
            ->setParameter(':languageId', $languageId)
2325
        ;
2326
2327
        if (null !== $versionNo) {
2328
            $query
2329
                ->andWhere('version = :versionNo')
2330
                ->setParameter(':versionNo', $versionNo)
2331
            ;
2332
        }
2333
2334
        $rowCount = $query->execute();
2335
2336
        // no rows updated means that most likely somehow it was the last remaining translation
2337
        if ($rowCount === 0) {
2338
            throw new BadStateException(
2339
                '$languageCode',
2340
                'Specified translation is the only one Content Object Version has'
2341
            );
2342
        }
2343
    }
2344
}
2345