Completed
Push — location_content_property ( b180d5 )
by André
16:28
created

DoctrineDatabase   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 2213
Duplicated Lines 8 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 0
Metric Value
dl 177
loc 2213
rs 0.5217
c 0
b 0
f 0
wmc 118
lcom 1
cbo 26

55 Methods

Rating   Name   Duplication   Size   Complexity  
A getContext() 0 7 1
A __construct() 0 13 1
A insertContentObject() 0 66 3
A generateLanguageMask() 7 17 4
A insertVersion() 0 57 1
D updateContent() 0 72 13
B updateVersion() 0 42 1
C updateAlwaysAvailableFlag() 0 118 7
A setStatus() 0 55 3
A insertNewField() 0 18 1
A insertExistingField() 0 13 1
B setInsertFieldValues() 0 46 1
A isLanguageAlwaysAvailable() 0 7 2
A updateField() 0 20 1
A setFieldUpdateValues() 0 21 1
B updateNonTranslatableField() 0 27 1
A load() 0 11 1
A loadContentList() 0 4 1
B internalLoadContent() 0 84 4
A internalLoadContentInfo() 0 16 1
A loadContentInfo() 9 9 2
A loadContentInfoList() 0 4 1
A loadContentInfoByRemoteId() 9 9 2
A loadVersionInfo() 20 20 1
A listVersionsForUser() 0 18 1
B listVersions() 0 27 3
A listVersionsHelper() 0 22 3
A listVersionNumbers() 0 19 1
A getLastVersionNumber() 0 19 1
A getAllLocationIds() 0 19 1
B getFieldIdsByType() 0 46 5
B deleteRelations() 0 37 2
B removeReverseFieldRelations() 0 50 4
B removeRelationFromRelationListField() 0 35 2
A removeRelationFromRelationField() 0 22 1
A deleteField() 0 14 1
A deleteFields() 0 22 2
A deleteVersions() 0 22 2
A deleteNames() 0 22 2
A setName() 0 61 2
B getLanguageQuery() 0 39 1
A deleteContent() 0 13 1
A loadRelations() 11 68 3
A loadReverseRelations() 11 49 2
B insertRelation() 31 31 1
B deleteRelation() 0 62 4
A getContentIdsByContentTypeId() 0 18 1
A loadVersionedNameData() 0 23 2
A copyRelations() 0 20 2
A deleteTranslationFromContent() 16 16 2
B deleteTranslatedFields() 24 24 2
A deleteTranslationFromVersion() 15 15 2
B deleteTranslationFromContentNames() 24 24 2
B deleteTranslationFromContentObject() 0 29 2
B deleteTranslationFromContentVersions() 0 44 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DoctrineDatabase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DoctrineDatabase, and based on these observations, apply Extract Interface, too.

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 eZ\Publish\Core\Base\Exceptions\BadStateException;
14
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway;
15
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder;
16
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
17
use eZ\Publish\Core\Persistence\Database\UpdateQuery;
18
use eZ\Publish\Core\Persistence\Database\InsertQuery;
19
use eZ\Publish\Core\Persistence\Database\SelectQuery;
20
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
21
use eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator as LanguageMaskGenerator;
22
use eZ\Publish\SPI\Persistence\Content;
23
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
24
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
25
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
26
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
27
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
28
use eZ\Publish\SPI\Persistence\Content\Field;
29
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
30
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
31
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
32
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
33
use DOMXPath;
34
use DOMDocument;
35
use PDO;
36
37
/**
38
 * Doctrine database based content gateway.
39
 */
40
class DoctrineDatabase extends Gateway
41
{
42
    /**
43
     * eZ Doctrine database handler.
44
     *
45
     * @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler
46
     */
47
    protected $dbHandler;
48
49
    /**
50
     * The native Doctrine connection.
51
     *
52
     * Meant to be used to transition from eZ/Zeta interface to Doctrine.
53
     *
54
     * @var \Doctrine\DBAL\Connection
55
     */
56
    protected $connection;
57
58
    /**
59
     * Query builder.
60
     *
61
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder
62
     */
63
    protected $queryBuilder;
64
65
    /**
66
     * Caching language handler.
67
     *
68
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler
69
     */
70
    protected $languageHandler;
71
72
    /**
73
     * Language mask generator.
74
     *
75
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator
76
     */
77
    protected $languageMaskGenerator;
78
79
    /**
80
     * Creates a new gateway based on $db.
81
     *
82
     * @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $db
83
     * @param \Doctrine\DBAL\Connection $connection
84
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase\QueryBuilder $queryBuilder
85
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
86
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $languageMaskGenerator
87
     */
88
    public function __construct(
89
        DatabaseHandler $db,
90
        Connection $connection,
91
        QueryBuilder $queryBuilder,
92
        LanguageHandler $languageHandler,
93
        LanguageMaskGenerator $languageMaskGenerator
94
    ) {
95
        $this->dbHandler = $db;
96
        $this->connection = $connection;
97
        $this->queryBuilder = $queryBuilder;
98
        $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...
99
        $this->languageMaskGenerator = $languageMaskGenerator;
100
    }
101
102
    /**
103
     * Get context definition for external storage layers.
104
     *
105
     * @return array
106
     */
107
    public function getContext()
108
    {
109
        return array(
110
            'identifier' => 'LegacyStorage',
111
            'connection' => $this->dbHandler,
112
        );
113
    }
114
115
    /**
116
     * Inserts a new content object.
117
     *
118
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct
119
     * @param mixed $currentVersionNo
120
     *
121
     * @return int ID
122
     */
123
    public function insertContentObject(CreateStruct $struct, $currentVersionNo = 1)
124
    {
125
        $initialLanguageId = !empty($struct->mainLanguageId) ? $struct->mainLanguageId : $struct->initialLanguageId;
126
        $initialLanguageCode = $this->languageHandler->load($initialLanguageId)->languageCode;
127
128
        if (isset($struct->name[$initialLanguageCode])) {
129
            $name = $struct->name[$initialLanguageCode];
130
        } else {
131
            $name = '';
132
        }
133
134
        $q = $this->dbHandler->createInsertQuery();
135
        $q->insertInto(
136
            $this->dbHandler->quoteTable('ezcontentobject')
137
        )->set(
138
            $this->dbHandler->quoteColumn('id'),
139
            $this->dbHandler->getAutoIncrementValue('ezcontentobject', 'id')
140
        )->set(
141
            $this->dbHandler->quoteColumn('current_version'),
142
            $q->bindValue($currentVersionNo, null, \PDO::PARAM_INT)
143
        )->set(
144
            $this->dbHandler->quoteColumn('name'),
145
            $q->bindValue($name, null, \PDO::PARAM_STR)
146
        )->set(
147
            $this->dbHandler->quoteColumn('contentclass_id'),
148
            $q->bindValue($struct->typeId, null, \PDO::PARAM_INT)
149
        )->set(
150
            $this->dbHandler->quoteColumn('section_id'),
151
            $q->bindValue($struct->sectionId, null, \PDO::PARAM_INT)
152
        )->set(
153
            $this->dbHandler->quoteColumn('owner_id'),
154
            $q->bindValue($struct->ownerId, null, \PDO::PARAM_INT)
155
        )->set(
156
            $this->dbHandler->quoteColumn('initial_language_id'),
157
            $q->bindValue($initialLanguageId, null, \PDO::PARAM_INT)
158
        )->set(
159
            $this->dbHandler->quoteColumn('remote_id'),
160
            $q->bindValue($struct->remoteId, null, \PDO::PARAM_STR)
161
        )->set(
162
            $this->dbHandler->quoteColumn('modified'),
163
            $q->bindValue(0, null, \PDO::PARAM_INT)
164
        )->set(
165
            $this->dbHandler->quoteColumn('published'),
166
            $q->bindValue(0, null, \PDO::PARAM_INT)
167
        )->set(
168
            $this->dbHandler->quoteColumn('status'),
169
            $q->bindValue(ContentInfo::STATUS_DRAFT, null, \PDO::PARAM_INT)
170
        )->set(
171
            $this->dbHandler->quoteColumn('language_mask'),
172
            $q->bindValue(
173
                $this->generateLanguageMask(
174
                    $struct->fields,
175
                    $initialLanguageCode,
176
                    $struct->alwaysAvailable
177
                ),
178
                null,
179
                \PDO::PARAM_INT
180
            )
181
        );
182
183
        $q->prepare()->execute();
184
185
        return $this->dbHandler->lastInsertId(
186
            $this->dbHandler->getSequenceName('ezcontentobject', 'id')
187
        );
188
    }
189
190
    /**
191
     * Generates a language mask for $version.
192
     *
193
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $fields
194
     * @param string $initialLanguageCode
195
     * @param bool $alwaysAvailable
196
     *
197
     * @return int
198
     */
199
    protected function generateLanguageMask(array $fields, $initialLanguageCode, $alwaysAvailable)
200
    {
201
        $languages = array($initialLanguageCode => true);
202 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...
203
            if (isset($languages[$field->languageCode])) {
204
                continue;
205
            }
206
207
            $languages[$field->languageCode] = true;
208
        }
209
210
        if ($alwaysAvailable) {
211
            $languages['always-available'] = true;
212
        }
213
214
        return $this->languageMaskGenerator->generateLanguageMask($languages);
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
            $languages = [];
333
            foreach ($prePublishVersionInfo->languageCodes as $languageCodes) {
334
                if (!isset($languages[$languageCodes])) {
335
                    $languages[$languageCodes] = true;
336
                }
337
            }
338
339
            $languages['always-available'] = isset($struct->alwaysAvailable) ? $struct->alwaysAvailable :
340
                $prePublishVersionInfo->contentInfo->alwaysAvailable;
341
342
            $mask = $this->languageMaskGenerator->generateLanguageMask($languages);
343
344
            $q->set(
345
                $this->dbHandler->quoteColumn('language_mask'),
346
                $q->bindValue($mask, null, \PDO::PARAM_INT)
347
            );
348
        }
349
        $q->where(
350
            $q->expr->eq(
351
                $this->dbHandler->quoteColumn('id'),
352
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
353
            )
354
        );
355
        $q->prepare()->execute();
356
357
        // Handle alwaysAvailable flag update separately as it's a more complex task and has impact on several tables
358
        if (isset($struct->alwaysAvailable) || isset($struct->mainLanguageId)) {
359
            $this->updateAlwaysAvailableFlag($contentId, $struct->alwaysAvailable);
360
        }
361
    }
362
363
    /**
364
     * Updates version $versionNo for content identified by $contentId, in respect to $struct.
365
     *
366
     * @param int $contentId
367
     * @param int $versionNo
368
     * @param \eZ\Publish\SPI\Persistence\Content\UpdateStruct $struct
369
     */
370
    public function updateVersion($contentId, $versionNo, UpdateStruct $struct)
371
    {
372
        $q = $this->dbHandler->createUpdateQuery();
373
        $q->update(
374
            $this->dbHandler->quoteTable('ezcontentobject_version')
375
        )->set(
376
            $this->dbHandler->quoteColumn('creator_id'),
377
            $q->bindValue($struct->creatorId, null, \PDO::PARAM_INT)
378
        )->set(
379
            $this->dbHandler->quoteColumn('modified'),
380
            $q->bindValue($struct->modificationDate, null, \PDO::PARAM_INT)
381
        )->set(
382
            $this->dbHandler->quoteColumn('initial_language_id'),
383
            $q->bindValue($struct->initialLanguageId, null, \PDO::PARAM_INT)
384
        )->set(
385
            $this->dbHandler->quoteColumn('language_mask'),
386
            $q->expr->bitOr(
387
                $this->dbHandler->quoteColumn('language_mask'),
388
                $q->bindValue(
389
                    $this->generateLanguageMask(
390
                        $struct->fields,
391
                        $this->languageHandler->load($struct->initialLanguageId)->languageCode,
392
                        false
393
                    ),
394
                    null,
395
                    \PDO::PARAM_INT
396
                )
397
            )
398
        )->where(
399
            $q->expr->lAnd(
400
                $q->expr->eq(
401
                    $this->dbHandler->quoteColumn('contentobject_id'),
402
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
403
                ),
404
                $q->expr->eq(
405
                    $this->dbHandler->quoteColumn('version'),
406
                    $q->bindValue($versionNo, null, \PDO::PARAM_INT)
407
                )
408
            )
409
        );
410
        $q->prepare()->execute();
411
    }
412
413
    /**
414
     * Updates "always available" flag for Content identified by $contentId, in respect to
415
     * Content's current main language and optionally new $alwaysAvailable state.
416
     *
417
     * @param int $contentId
418
     * @param bool|null $alwaysAvailable New "always available" value or null if not defined
419
     */
420
    public function updateAlwaysAvailableFlag($contentId, $alwaysAvailable = null)
421
    {
422
        // We will need to know some info on the current language mask to update the flag
423
        // everywhere needed
424
        $contentInfoRow = $this->loadContentInfo($contentId);
425
        if (!isset($alwaysAvailable)) {
426
            $alwaysAvailable = (bool)$contentInfoRow['language_mask'] & 1;
427
        }
428
429
        /** @var $q \eZ\Publish\Core\Persistence\Database\UpdateQuery */
430
        $q = $this->dbHandler->createUpdateQuery();
431
        $q
432
            ->update($this->dbHandler->quoteTable('ezcontentobject'))
433
            ->set(
434
                $this->dbHandler->quoteColumn('language_mask'),
435
                $alwaysAvailable ?
436
                    $q->expr->bitOr($this->dbHandler->quoteColumn('language_mask'), 1) :
437
                    $q->expr->bitAnd($this->dbHandler->quoteColumn('language_mask'), -2)
438
            )
439
            ->where(
440
                $q->expr->eq(
441
                    $this->dbHandler->quoteColumn('id'),
442
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
443
                )
444
            );
445
        $q->prepare()->execute();
446
447
        // Now we need to update ezcontentobject_name
448
        /** @var $qName \eZ\Publish\Core\Persistence\Database\UpdateQuery */
449
        $qName = $this->dbHandler->createUpdateQuery();
450
        $qName
451
            ->update($this->dbHandler->quoteTable('ezcontentobject_name'))
452
            ->set(
453
                $this->dbHandler->quoteColumn('language_id'),
454
                $alwaysAvailable ?
455
                    $qName->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
456
                    $qName->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
457
            )
458
            ->where(
459
                $qName->expr->lAnd(
460
                    $qName->expr->eq(
461
                        $this->dbHandler->quoteColumn('contentobject_id'),
462
                        $qName->bindValue($contentId, null, \PDO::PARAM_INT)
463
                    ),
464
                    $qName->expr->eq(
465
                        $this->dbHandler->quoteColumn('content_version'),
466
                        $qName->bindValue(
467
                            $contentInfoRow['current_version'],
468
                            null,
469
                            \PDO::PARAM_INT
470
                        )
471
                    )
472
                )
473
            );
474
        $qName->prepare()->execute();
475
476
        // Now update ezcontentobject_attribute for current version
477
        // Create update query that will be reused
478
        /** @var $qAttr \eZ\Publish\Core\Persistence\Database\UpdateQuery */
479
        $qAttr = $this->dbHandler->createUpdateQuery();
480
        $qAttr
481
            ->update($this->dbHandler->quoteTable('ezcontentobject_attribute'))
482
            ->where(
483
                $qAttr->expr->lAnd(
484
                    $qAttr->expr->eq(
485
                        $this->dbHandler->quoteColumn('contentobject_id'),
486
                        $qAttr->bindValue($contentId, null, \PDO::PARAM_INT)
487
                    ),
488
                    $qAttr->expr->eq(
489
                        $this->dbHandler->quoteColumn('version'),
490
                        $qAttr->bindValue(
491
                            $contentInfoRow['current_version'],
492
                            null,
493
                            \PDO::PARAM_INT
494
                        )
495
                    )
496
                )
497
            );
498
499
        // If there is only a single language, update all fields and return
500
        if (!$this->languageMaskGenerator->isLanguageMaskComposite($contentInfoRow['language_mask'])) {
501
            $qAttr->set(
502
                $this->dbHandler->quoteColumn('language_id'),
503
                $alwaysAvailable ?
504
                    $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1) :
505
                    $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
506
            );
507
            $qAttr->prepare()->execute();
508
509
            return;
510
        }
511
512
        // Otherwise:
513
        // 1. Remove always available flag on all fields
514
        $qAttr->set(
515
            $this->dbHandler->quoteColumn('language_id'),
516
            $qAttr->expr->bitAnd($this->dbHandler->quoteColumn('language_id'), -2)
517
        );
518
        $qAttr->prepare()->execute();
519
520
        // 2. If Content is always available set the flag only on fields in main language
521
        if ($alwaysAvailable) {
522
            $qAttr->set(
523
                $this->dbHandler->quoteColumn('language_id'),
524
                $qAttr->expr->bitOr($this->dbHandler->quoteColumn('language_id'), 1)
525
            );
526
            $qAttr->where(
527
                $qAttr->expr->gt(
528
                    $qAttr->expr->bitAnd(
529
                        $this->dbHandler->quoteColumn('language_id'),
530
                        $qAttr->bindValue($contentInfoRow['initial_language_id'], null, PDO::PARAM_INT)
531
                    ),
532
                    $qAttr->bindValue(0, null, PDO::PARAM_INT)
533
                )
534
            );
535
            $qAttr->prepare()->execute();
536
        }
537
    }
538
539
    /**
540
     * Sets the status of the version identified by $contentId and $version to $status.
541
     *
542
     * The $status can be one of STATUS_DRAFT, STATUS_PUBLISHED, STATUS_ARCHIVED
543
     *
544
     * @param int $contentId
545
     * @param int $version
546
     * @param int $status
547
     *
548
     * @return bool
549
     */
550
    public function setStatus($contentId, $version, $status)
551
    {
552
        $q = $this->dbHandler->createUpdateQuery();
553
        $q->update(
554
            $this->dbHandler->quoteTable('ezcontentobject_version')
555
        )->set(
556
            $this->dbHandler->quoteColumn('status'),
557
            $q->bindValue($status, null, \PDO::PARAM_INT)
558
        )->set(
559
            $this->dbHandler->quoteColumn('modified'),
560
            $q->bindValue(time(), null, \PDO::PARAM_INT)
561
        )->where(
562
            $q->expr->lAnd(
563
                $q->expr->eq(
564
                    $this->dbHandler->quoteColumn('contentobject_id'),
565
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
566
                ),
567
                $q->expr->eq(
568
                    $this->dbHandler->quoteColumn('version'),
569
                    $q->bindValue($version, null, \PDO::PARAM_INT)
570
                )
571
            )
572
        );
573
        $statement = $q->prepare();
574
        $statement->execute();
575
576
        if ((bool)$statement->rowCount() === false) {
577
            return false;
578
        }
579
580
        if ($status !== APIVersionInfo::STATUS_PUBLISHED) {
581
            return true;
582
        }
583
584
        // If the version's status is PUBLISHED, we set the content to published status as well
585
        $q = $this->dbHandler->createUpdateQuery();
586
        $q->update(
587
            $this->dbHandler->quoteTable('ezcontentobject')
588
        )->set(
589
            $this->dbHandler->quoteColumn('status'),
590
            $q->bindValue(ContentInfo::STATUS_PUBLISHED, null, \PDO::PARAM_INT)
591
        )->set(
592
            $this->dbHandler->quoteColumn('current_version'),
593
            $q->bindValue($version, null, \PDO::PARAM_INT)
594
        )->where(
595
            $q->expr->eq(
596
                $this->dbHandler->quoteColumn('id'),
597
                $q->bindValue($contentId, null, \PDO::PARAM_INT)
598
            )
599
        );
600
        $statement = $q->prepare();
601
        $statement->execute();
602
603
        return (bool)$statement->rowCount();
604
    }
605
606
    /**
607
     * Inserts a new field.
608
     *
609
     * Only used when a new field is created (i.e. a new object or a field in a
610
     * new language!). After that, field IDs need to stay the same, only the
611
     * version number changes.
612
     *
613
     * @param \eZ\Publish\SPI\Persistence\Content $content
614
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
615
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
616
     *
617
     * @return int ID
618
     */
619
    public function insertNewField(Content $content, Field $field, StorageFieldValue $value)
620
    {
621
        $q = $this->dbHandler->createInsertQuery();
622
623
        $this->setInsertFieldValues($q, $content, $field, $value);
624
625
        // Insert with auto increment ID
626
        $q->set(
627
            $this->dbHandler->quoteColumn('id'),
628
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_attribute', 'id')
629
        );
630
631
        $q->prepare()->execute();
632
633
        return $this->dbHandler->lastInsertId(
634
            $this->dbHandler->getSequenceName('ezcontentobject_attribute', 'id')
635
        );
636
    }
637
638
    /**
639
     * Inserts an existing field.
640
     *
641
     * Used to insert a field with an exsting ID but a new version number.
642
     *
643
     * @param Content $content
644
     * @param Field $field
645
     * @param StorageFieldValue $value
646
     */
647
    public function insertExistingField(Content $content, Field $field, StorageFieldValue $value)
648
    {
649
        $q = $this->dbHandler->createInsertQuery();
650
651
        $this->setInsertFieldValues($q, $content, $field, $value);
652
653
        $q->set(
654
            $this->dbHandler->quoteColumn('id'),
655
            $q->bindValue($field->id, null, \PDO::PARAM_INT)
656
        );
657
658
        $q->prepare()->execute();
659
    }
660
661
    /**
662
     * Sets field (ezcontentobject_attribute) values to the given query.
663
     *
664
     * @param \eZ\Publish\Core\Persistence\Database\InsertQuery $q
665
     * @param Content $content
666
     * @param Field $field
667
     * @param StorageFieldValue $value
668
     */
669
    protected function setInsertFieldValues(InsertQuery $q, Content $content, Field $field, StorageFieldValue $value)
670
    {
671
        $q->insertInto(
672
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
673
        )->set(
674
            $this->dbHandler->quoteColumn('contentobject_id'),
675
            $q->bindValue($content->versionInfo->contentInfo->id, null, \PDO::PARAM_INT)
676
        )->set(
677
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
678
            $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
679
        )->set(
680
            $this->dbHandler->quoteColumn('data_type_string'),
681
            $q->bindValue($field->type)
682
        )->set(
683
            $this->dbHandler->quoteColumn('language_code'),
684
            $q->bindValue($field->languageCode)
685
        )->set(
686
            $this->dbHandler->quoteColumn('version'),
687
            $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
688
        )->set(
689
            $this->dbHandler->quoteColumn('data_float'),
690
            $q->bindValue($value->dataFloat)
691
        )->set(
692
            $this->dbHandler->quoteColumn('data_int'),
693
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
694
        )->set(
695
            $this->dbHandler->quoteColumn('data_text'),
696
            $q->bindValue($value->dataText)
697
        )->set(
698
            $this->dbHandler->quoteColumn('sort_key_int'),
699
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
700
        )->set(
701
            $this->dbHandler->quoteColumn('sort_key_string'),
702
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
703
        )->set(
704
            $this->dbHandler->quoteColumn('language_id'),
705
            $q->bindValue(
706
                $this->languageMaskGenerator->generateLanguageIndicator(
707
                    $field->languageCode,
708
                    $this->isLanguageAlwaysAvailable($content, $field->languageCode)
709
                ),
710
                null,
711
                \PDO::PARAM_INT
712
            )
713
        );
714
    }
715
716
    /**
717
     * Checks if $languageCode is always available in $content.
718
     *
719
     * @param \eZ\Publish\SPI\Persistence\Content $content
720
     * @param string $languageCode
721
     *
722
     * @return bool
723
     */
724
    protected function isLanguageAlwaysAvailable(Content $content, $languageCode)
725
    {
726
        return
727
            $content->versionInfo->contentInfo->alwaysAvailable &&
728
            $content->versionInfo->contentInfo->mainLanguageCode === $languageCode
729
        ;
730
    }
731
732
    /**
733
     * Updates an existing field.
734
     *
735
     * @param Field $field
736
     * @param StorageFieldValue $value
737
     */
738
    public function updateField(Field $field, StorageFieldValue $value)
739
    {
740
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
741
        // cannot change on update
742
        $q = $this->dbHandler->createUpdateQuery();
743
        $this->setFieldUpdateValues($q, $value);
744
        $q->where(
745
            $q->expr->lAnd(
746
                $q->expr->eq(
747
                    $this->dbHandler->quoteColumn('id'),
748
                    $q->bindValue($field->id, null, \PDO::PARAM_INT)
749
                ),
750
                $q->expr->eq(
751
                    $this->dbHandler->quoteColumn('version'),
752
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
753
                )
754
            )
755
        );
756
        $q->prepare()->execute();
757
    }
758
759
    /**
760
     * Sets update fields for $value on $q.
761
     *
762
     * @param \eZ\Publish\Core\Persistence\Database\UpdateQuery $q
763
     * @param StorageFieldValue $value
764
     */
765
    protected function setFieldUpdateValues(UpdateQuery $q, StorageFieldValue $value)
766
    {
767
        $q->update(
768
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
769
        )->set(
770
            $this->dbHandler->quoteColumn('data_float'),
771
            $q->bindValue($value->dataFloat)
772
        )->set(
773
            $this->dbHandler->quoteColumn('data_int'),
774
            $q->bindValue($value->dataInt, null, \PDO::PARAM_INT)
775
        )->set(
776
            $this->dbHandler->quoteColumn('data_text'),
777
            $q->bindValue($value->dataText)
778
        )->set(
779
            $this->dbHandler->quoteColumn('sort_key_int'),
780
            $q->bindValue($value->sortKeyInt, null, \PDO::PARAM_INT)
781
        )->set(
782
            $this->dbHandler->quoteColumn('sort_key_string'),
783
            $q->bindValue(mb_substr($value->sortKeyString, 0, 255))
784
        );
785
    }
786
787
    /**
788
     * Updates an existing, non-translatable field.
789
     *
790
     * @param \eZ\Publish\SPI\Persistence\Content\Field $field
791
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue $value
792
     * @param int $contentId
793
     */
794
    public function updateNonTranslatableField(
795
        Field $field,
796
        StorageFieldValue $value,
797
        $contentId
798
    ) {
799
        // Note, no need to care for language_id here, since Content->$alwaysAvailable
800
        // cannot change on update
801
        $q = $this->dbHandler->createUpdateQuery();
802
        $this->setFieldUpdateValues($q, $value);
803
        $q->where(
804
            $q->expr->lAnd(
805
                $q->expr->eq(
806
                    $this->dbHandler->quoteColumn('contentclassattribute_id'),
807
                    $q->bindValue($field->fieldDefinitionId, null, \PDO::PARAM_INT)
808
                ),
809
                $q->expr->eq(
810
                    $this->dbHandler->quoteColumn('contentobject_id'),
811
                    $q->bindValue($contentId, null, \PDO::PARAM_INT)
812
                ),
813
                $q->expr->eq(
814
                    $this->dbHandler->quoteColumn('version'),
815
                    $q->bindValue($field->versionNo, null, \PDO::PARAM_INT)
816
                )
817
            )
818
        );
819
        $q->prepare()->execute();
820
    }
821
822
    /**
823
     * Loads data for a content object.
824
     *
825
     * Returns an array with the relevant data.
826
     *
827
     * @param mixed $contentId
828
     * @param mixed $version
829
     * @param string[]|null $translations
830
     *
831
     * @return array
832
     */
833
    public function load($contentId, $version, array $translations = null)
834
    {
835
        $results = $this->internalLoadContent([
836
            ['id' => $contentId, 'version' => $version, 'languages' => $translations]
837
        ]);
838
        //if (empty($results)) {
839
            // @todo: Should we add a throw here to cleanup and be consistent with other load methods?
840
        //}
841
842
        return $results;
843
    }
844
845
    /**
846
     * Loads current version for a list of content objects.
847
     *
848
     * @param array[] $IdVersionTranslationPairs Hashes with 'id', optionally 'version', & optionally 'languages'
849
     *                If version is not set current version will be loaded, if languages is not set ALL will be loaded.
850
     *
851
     * @return array[]
852
     */
853
    public function loadContentList(array $IdVersionTranslationPairs): array
854
    {
855
        return $this->internalLoadContent($IdVersionTranslationPairs);
856
    }
857
858
    /**
859
     * @see load(), loadContentList()
860
     *
861
     * @param array[] $IdVersionTranslationPairs Hashes with 'id', optionally 'version', & optionally 'languages'
862
     *                If version is not set current version will be loaded, if languages is not set ALL will be loaded.
863
     *
864
     * @return array
865
     */
866
    private function internalLoadContent(array $IdVersionTranslationPairs): array
867
    {
868
        $q = $this->connection->createQueryBuilder();
869
        $q
870
            ->select(
871
                'c.id AS ezcontentobject_id',
872
                'c.contentclass_id AS ezcontentobject_contentclass_id',
873
                'c.section_id AS ezcontentobject_section_id',
874
                'c.owner_id AS ezcontentobject_owner_id',
875
                'c.remote_id AS ezcontentobject_remote_id',
876
                'c.current_version AS ezcontentobject_current_version',
877
                'c.initial_language_id AS ezcontentobject_initial_language_id',
878
                'c.modified AS ezcontentobject_modified',
879
                'c.published AS ezcontentobject_published',
880
                'c.status AS ezcontentobject_status',
881
                'c.name AS ezcontentobject_name',
882
                'c.language_mask AS ezcontentobject_language_mask',
883
                'v.id AS ezcontentobject_version_id',
884
                'v.version AS ezcontentobject_version_version',
885
                'v.modified AS ezcontentobject_version_modified',
886
                'v.creator_id AS ezcontentobject_version_creator_id',
887
                'v.created AS ezcontentobject_version_created',
888
                'v.status AS ezcontentobject_version_status',
889
                'v.language_mask AS ezcontentobject_version_language_mask',
890
                'v.initial_language_id AS ezcontentobject_version_initial_language_id',
891
                'a.id AS ezcontentobject_attribute_id',
892
                'a.contentclassattribute_id AS ezcontentobject_attribute_contentclassattribute_id',
893
                'a.data_type_string AS ezcontentobject_attribute_data_type_string',
894
                'a.language_code AS ezcontentobject_attribute_language_code',
895
                'a.language_id AS ezcontentobject_attribute_language_id',
896
                'a.data_float AS ezcontentobject_attribute_data_float',
897
                'a.data_int AS ezcontentobject_attribute_data_int',
898
                'a.data_text AS ezcontentobject_attribute_data_text',
899
                'a.sort_key_int AS ezcontentobject_attribute_sort_key_int',
900
                'a.sort_key_string AS ezcontentobject_attribute_sort_key_string',
901
                't.main_node_id AS ezcontentobject_tree_main_node_id'
902
            )
903
            ->from('ezcontentobject', 'c')
904
            ->innerJoin(
905
                'c',
906
                'ezcontentobject_version',
907
                'v',
908
                'c.id = v.contentobject_id'
909
            )
910
            ->innerJoin(
911
                'v',
912
                'ezcontentobject_attribute',
913
                'a',
914
                'v.contentobject_id = a.contentobject_id AND v.version = a.version'
915
            )
916
            ->leftJoin(
917
                'c',
918
                'ezcontentobject_tree',
919
                't',
920
                'c.id = t.contentobject_id AND t.node_id = t.main_node_id'
921
            );
922
923
        $where = [];
924
        $expr = $q->expr();
925
        foreach ($IdVersionTranslationPairs as $IdVersionTranslation) {
926
            $clauses = [
927
                $expr->eq('c.id', $q->createNamedParameter($IdVersionTranslation['id'], PDO::PARAM_INT)),
928
                empty($IdVersionTranslation['version']) ?
929
                    $expr->eq('v.version', 'c.current_version') :
930
                    $expr->eq('v.version', $q->createNamedParameter($IdVersionTranslation['version'], PDO::PARAM_INT))
931
932
            ];
933
934
            if (!empty($IdVersionTranslation['languages'])) {
935
                $clauses[] = $expr->in(
936
                    'a.language_code',
937
                    $q->createNamedParameter($IdVersionTranslation['languages'], Connection::PARAM_STR_ARRAY)
938
                );
939
            }
940
941
            $where[] = $expr->andX(...$clauses);
942
        }
943
944
        $q->where(
945
            $expr->orX(...$where)
946
        );
947
948
        return $q->execute()->fetchAll();
949
    }
950
951
    /**
952
     * @see loadContentInfo(), loadContentInfoByRemoteId(), loadContentInfoList()
953
     *
954
     * @param string $column
955
     * @param array $ids Array of int, or if $bindype is set to Connection::PARAM_STR_ARRAY then array of string
956
     * @param int $bindType One of Connection::PARAM_*_ARRAY constants.
957
     *
958
     * @return array
959
     */
960
    private function internalLoadContentInfo(string $column, array $ids, int $bindType = Connection::PARAM_INT_ARRAY): array
961
    {
962
        $q = $this->connection->createQueryBuilder();
963
        $q
964
            ->select('c.*', 't.main_node_id AS ezcontentobject_tree_main_node_id')
965
            ->from('ezcontentobject', 'c')
966
            ->leftJoin(
967
                'c',
968
                'ezcontentobject_tree',
969
                't',
970
                'c.id = t.contentobject_id AND t.node_id = t.main_node_id'
971
            )->where("c.${column} IN (:ids)")
972
            ->setParameter(':ids', $ids, $bindType);
973
974
        return $q->execute()->fetchAll();
975
    }
976
977
    /**
978
     * Loads info for content identified by $contentId.
979
     * Will basically return a hash containing all field values for ezcontentobject table plus some additional keys:
980
     *  - always_available => Boolean indicating if content's language mask contains alwaysAvailable bit field
981
     *  - main_language_code => Language code for main (initial) language. E.g. "eng-GB".
982
     *
983
     * @param int $contentId
984
     *
985
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
986
     *
987
     * @return array
988
     */
989 View Code Duplication
    public function loadContentInfo($contentId)
990
    {
991
        $results = $this->internalLoadContentInfo('id', [$contentId]);
992
        if (empty($results)) {
993
            throw new NotFound('content', "id: $contentId");
994
        }
995
996
        return $results[0];
997
    }
998
999
    public function loadContentInfoList(array $contentIds)
1000
    {
1001
        return $this->internalLoadContentInfo('id', $contentIds);
1002
    }
1003
1004
    /**
1005
     * Loads info for a content object identified by its remote ID.
1006
     *
1007
     * Returns an array with the relevant data.
1008
     *
1009
     * @param mixed $remoteId
1010
     *
1011
     * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException
1012
     *
1013
     * @return array
1014
     */
1015 View Code Duplication
    public function loadContentInfoByRemoteId($remoteId)
1016
    {
1017
        $results = $this->internalLoadContentInfo('remote_id', [$remoteId], Connection::PARAM_STR_ARRAY);
1018
        if (empty($results)) {
1019
            throw new NotFound('content', "remote_id: $remoteId");
1020
        }
1021
1022
        return $results[0];
1023
    }
1024
1025
    /**
1026
     * Loads version info for content identified by $contentId and $versionNo.
1027
     * Will basically return a hash containing all field values from ezcontentobject_version table plus following keys:
1028
     *  - names => Hash of content object names. Key is the language code, value is the name.
1029
     *  - 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.
1030
     *  - initial_language_code => Language code for initial language in this version.
1031
     *
1032
     * @param int $contentId
1033
     * @param int $versionNo
1034
     *
1035
     * @return array
1036
     */
1037 View Code Duplication
    public function loadVersionInfo($contentId, $versionNo)
1038
    {
1039
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1040
        $query->where(
1041
            $query->expr->lAnd(
1042
                $query->expr->eq(
1043
                    $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1044
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1045
                ),
1046
                $query->expr->eq(
1047
                    $this->dbHandler->quoteColumn('version', 'ezcontentobject_version'),
1048
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1049
                )
1050
            )
1051
        );
1052
        $statement = $query->prepare();
1053
        $statement->execute();
1054
1055
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1056
    }
1057
1058
    /**
1059
     * Returns data for all versions with given status created by the given $userId.
1060
     *
1061
     * @param int $userId
1062
     * @param int $status
1063
     *
1064
     * @return string[][]
1065
     */
1066
    public function listVersionsForUser($userId, $status = VersionInfo::STATUS_DRAFT)
1067
    {
1068
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1069
        $query->where(
1070
            $query->expr->lAnd(
1071
                $query->expr->eq(
1072
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1073
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1074
                ),
1075
                $query->expr->eq(
1076
                    $this->dbHandler->quoteColumn('creator_id', 'ezcontentobject_version'),
1077
                    $query->bindValue($userId, null, \PDO::PARAM_INT)
1078
                )
1079
            )
1080
        );
1081
1082
        return $this->listVersionsHelper($query);
1083
    }
1084
1085
    /**
1086
     * Returns all version data for the given $contentId, optionally filtered by status.
1087
     *
1088
     * Result is returned with oldest version first (using version id as it has index and is auto increment).
1089
     *
1090
     * @param mixed $contentId
1091
     * @param mixed|null $status Optional argument to filter versions by status, like {@see VersionInfo::STATUS_ARCHIVED}.
1092
     * @param int $limit Limit for items returned, -1 means none.
1093
     *
1094
     * @return string[][]
1095
     */
1096
    public function listVersions($contentId, $status = null, $limit = -1)
1097
    {
1098
        $query = $this->queryBuilder->createVersionInfoFindQuery();
1099
1100
        $filter = $query->expr->eq(
1101
            $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_version'),
1102
            $query->bindValue($contentId, null, \PDO::PARAM_INT)
1103
        );
1104
1105
        if ($status !== null) {
1106
            $filter = $query->expr->lAnd(
1107
                $filter,
1108
                $query->expr->eq(
1109
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_version'),
1110
                    $query->bindValue($status, null, \PDO::PARAM_INT)
1111
                )
1112
            );
1113
        }
1114
1115
        $query->where($filter);
1116
1117
        if ($limit > 0) {
1118
            $query->limit($limit);
1119
        }
1120
1121
        return $this->listVersionsHelper($query);
1122
    }
1123
1124
    /**
1125
     * Helper for {@see listVersions()} and {@see listVersionsForUser()} that filters duplicates
1126
     * that are the result of the cartesian product performed by createVersionInfoFindQuery().
1127
     *
1128
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
1129
     *
1130
     * @return string[][]
1131
     */
1132
    private function listVersionsHelper(SelectQuery $query)
1133
    {
1134
        $query->orderBy(
1135
            $this->dbHandler->quoteColumn('id', 'ezcontentobject_version')
1136
        );
1137
1138
        $statement = $query->prepare();
1139
        $statement->execute();
1140
1141
        $results = array();
1142
        $previousId = null;
1143
        foreach ($statement->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1144
            if ($row['ezcontentobject_version_id'] == $previousId) {
1145
                continue;
1146
            }
1147
1148
            $previousId = $row['ezcontentobject_version_id'];
1149
            $results[] = $row;
1150
        }
1151
1152
        return $results;
1153
    }
1154
1155
    /**
1156
     * Returns all version numbers for the given $contentId.
1157
     *
1158
     * @param mixed $contentId
1159
     *
1160
     * @return int[]
1161
     */
1162
    public function listVersionNumbers($contentId)
1163
    {
1164
        $query = $this->dbHandler->createSelectQuery();
1165
        $query->selectDistinct(
1166
            $this->dbHandler->quoteColumn('version')
1167
        )->from(
1168
            $this->dbHandler->quoteTable('ezcontentobject_version')
1169
        )->where(
1170
            $query->expr->eq(
1171
                $this->dbHandler->quoteColumn('contentobject_id'),
1172
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1173
            )
1174
        );
1175
1176
        $statement = $query->prepare();
1177
        $statement->execute();
1178
1179
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1180
    }
1181
1182
    /**
1183
     * Returns last version number for content identified by $contentId.
1184
     *
1185
     * @param int $contentId
1186
     *
1187
     * @return int
1188
     */
1189
    public function getLastVersionNumber($contentId)
1190
    {
1191
        $query = $this->dbHandler->createSelectQuery();
1192
        $query->select(
1193
            $query->expr->max($this->dbHandler->quoteColumn('version'))
1194
        )->from(
1195
            $this->dbHandler->quoteTable('ezcontentobject_version')
1196
        )->where(
1197
            $query->expr->eq(
1198
                $this->dbHandler->quoteColumn('contentobject_id'),
1199
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1200
            )
1201
        );
1202
1203
        $statement = $query->prepare();
1204
        $statement->execute();
1205
1206
        return (int)$statement->fetchColumn();
1207
    }
1208
1209
    /**
1210
     * Returns all IDs for locations that refer to $contentId.
1211
     *
1212
     * @param int $contentId
1213
     *
1214
     * @return int[]
1215
     */
1216
    public function getAllLocationIds($contentId)
1217
    {
1218
        $query = $this->dbHandler->createSelectQuery();
1219
        $query->select(
1220
            $this->dbHandler->quoteColumn('node_id')
1221
        )->from(
1222
            $this->dbHandler->quoteTable('ezcontentobject_tree')
1223
        )->where(
1224
            $query->expr->eq(
1225
                $this->dbHandler->quoteColumn('contentobject_id'),
1226
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1227
            )
1228
        );
1229
1230
        $statement = $query->prepare();
1231
        $statement->execute();
1232
1233
        return $statement->fetchAll(\PDO::FETCH_COLUMN);
1234
    }
1235
1236
    /**
1237
     * Returns all field IDs of $contentId grouped by their type.
1238
     * If $versionNo is set only field IDs for that version are returned.
1239
     * If $languageCode is set, only field IDs for that language are returned.
1240
     *
1241
     * @param int $contentId
1242
     * @param int|null $versionNo
1243
     * @param string|null $languageCode
1244
     *
1245
     * @return int[][]
1246
     */
1247
    public function getFieldIdsByType($contentId, $versionNo = null, $languageCode = null)
1248
    {
1249
        $query = $this->dbHandler->createSelectQuery();
1250
        $query->select(
1251
            $this->dbHandler->quoteColumn('id'),
1252
            $this->dbHandler->quoteColumn('data_type_string')
1253
        )->from(
1254
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1255
        )->where(
1256
            $query->expr->eq(
1257
                $this->dbHandler->quoteColumn('contentobject_id'),
1258
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1259
            )
1260
        );
1261
1262
        if (isset($versionNo)) {
1263
            $query->where(
1264
                $query->expr->eq(
1265
                    $this->dbHandler->quoteColumn('version'),
1266
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1267
                )
1268
            );
1269
        }
1270
1271
        if (isset($languageCode)) {
1272
            $query->where(
1273
                $query->expr->eq(
1274
                    $this->dbHandler->quoteColumn('language_code'),
1275
                    $query->bindValue($languageCode, null, \PDO::PARAM_STR)
1276
                )
1277
            );
1278
        }
1279
1280
        $statement = $query->prepare();
1281
        $statement->execute();
1282
1283
        $result = array();
1284
        foreach ($statement->fetchAll() as $row) {
1285
            if (!isset($result[$row['data_type_string']])) {
1286
                $result[$row['data_type_string']] = array();
1287
            }
1288
            $result[$row['data_type_string']][] = (int)$row['id'];
1289
        }
1290
1291
        return $result;
1292
    }
1293
1294
    /**
1295
     * Deletes relations to and from $contentId.
1296
     * If $versionNo is set only relations for that version are deleted.
1297
     *
1298
     * @param int $contentId
1299
     * @param int|null $versionNo
1300
     */
1301
    public function deleteRelations($contentId, $versionNo = null)
1302
    {
1303
        $query = $this->dbHandler->createDeleteQuery();
1304
        $query->deleteFrom(
1305
            $this->dbHandler->quoteTable('ezcontentobject_link')
1306
        );
1307
1308
        if (isset($versionNo)) {
1309
            $query->where(
1310
                $query->expr->lAnd(
1311
                    $query->expr->eq(
1312
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1313
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1314
                    ),
1315
                    $query->expr->eq(
1316
                        $this->dbHandler->quoteColumn('from_contentobject_version'),
1317
                        $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1318
                    )
1319
                )
1320
            );
1321
        } else {
1322
            $query->where(
1323
                $query->expr->lOr(
1324
                    $query->expr->eq(
1325
                        $this->dbHandler->quoteColumn('from_contentobject_id'),
1326
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1327
                    ),
1328
                    $query->expr->eq(
1329
                        $this->dbHandler->quoteColumn('to_contentobject_id'),
1330
                        $query->bindValue($contentId, null, \PDO::PARAM_INT)
1331
                    )
1332
                )
1333
            );
1334
        }
1335
1336
        $query->prepare()->execute();
1337
    }
1338
1339
    /**
1340
     * Removes relations to Content with $contentId from Relation and RelationList field type fields.
1341
     *
1342
     * @param int $contentId
1343
     */
1344
    public function removeReverseFieldRelations($contentId)
1345
    {
1346
        $query = $this->dbHandler->createSelectQuery();
1347
        $query
1348
            ->select('ezcontentobject_attribute.*')
1349
            ->from('ezcontentobject_attribute')
1350
            ->innerJoin(
1351
                'ezcontentobject_link',
1352
                $query->expr->lAnd(
1353
                    $query->expr->eq(
1354
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1355
                        $this->dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_attribute')
1356
                    ),
1357
                    $query->expr->eq(
1358
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1359
                        $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute')
1360
                    ),
1361
                    $query->expr->eq(
1362
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_link'),
1363
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_attribute')
1364
                    )
1365
                )
1366
            )
1367
            ->where(
1368
                $query->expr->eq(
1369
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1370
                    $query->bindValue($contentId, null, PDO::PARAM_INT)
1371
                ),
1372
                $query->expr->gt(
1373
                    $query->expr->bitAnd(
1374
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1375
                        $query->bindValue(8, null, PDO::PARAM_INT)
1376
                    ),
1377
                    0
1378
                )
1379
            );
1380
1381
        $statement = $query->prepare();
1382
        $statement->execute();
1383
1384
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
1385
            if ($row['data_type_string'] === 'ezobjectrelation') {
1386
                $this->removeRelationFromRelationField($row);
1387
            }
1388
1389
            if ($row['data_type_string'] === 'ezobjectrelationlist') {
1390
                $this->removeRelationFromRelationListField($contentId, $row);
1391
            }
1392
        }
1393
    }
1394
1395
    /**
1396
     * Updates field value of RelationList field type identified by given $row data,
1397
     * removing relations toward given $contentId.
1398
     *
1399
     * @param int $contentId
1400
     * @param array $row
1401
     */
1402
    protected function removeRelationFromRelationListField($contentId, array $row)
1403
    {
1404
        $document = new DOMDocument('1.0', 'utf-8');
1405
        $document->loadXML($row['data_text']);
1406
1407
        $xpath = new DOMXPath($document);
1408
        $xpathExpression = "//related-objects/relation-list/relation-item[@contentobject-id='{$contentId}']";
1409
1410
        $relationItems = $xpath->query($xpathExpression);
1411
        foreach ($relationItems as $relationItem) {
1412
            $relationItem->parentNode->removeChild($relationItem);
1413
        }
1414
1415
        $query = $this->dbHandler->createUpdateQuery();
1416
        $query
1417
            ->update('ezcontentobject_attribute')
1418
            ->set(
1419
                'data_text',
1420
                $query->bindValue($document->saveXML(), null, PDO::PARAM_STR)
1421
            )
1422
            ->where(
1423
                $query->expr->lAnd(
1424
                    $query->expr->eq(
1425
                        $this->dbHandler->quoteColumn('id'),
1426
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1427
                    ),
1428
                    $query->expr->eq(
1429
                        $this->dbHandler->quoteColumn('version'),
1430
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1431
                    )
1432
                )
1433
            );
1434
1435
        $query->prepare()->execute();
1436
    }
1437
1438
    /**
1439
     * Updates field value of Relation field type identified by given $row data,
1440
     * removing relation data.
1441
     *
1442
     * @param array $row
1443
     */
1444
    protected function removeRelationFromRelationField(array $row)
1445
    {
1446
        $query = $this->dbHandler->createUpdateQuery();
1447
        $query
1448
            ->update('ezcontentobject_attribute')
1449
            ->set('data_int', $query->bindValue(null, null, PDO::PARAM_INT))
1450
            ->set('sort_key_int', $query->bindValue(0, null, PDO::PARAM_INT))
1451
            ->where(
1452
                $query->expr->lAnd(
1453
                    $query->expr->eq(
1454
                        $this->dbHandler->quoteColumn('id'),
1455
                        $query->bindValue($row['id'], null, PDO::PARAM_INT)
1456
                    ),
1457
                    $query->expr->eq(
1458
                        $this->dbHandler->quoteColumn('version'),
1459
                        $query->bindValue($row['version'], null, PDO::PARAM_INT)
1460
                    )
1461
                )
1462
            );
1463
1464
        $query->prepare()->execute();
1465
    }
1466
1467
    /**
1468
     * Deletes the field with the given $fieldId.
1469
     *
1470
     * @param int $fieldId
1471
     */
1472
    public function deleteField($fieldId)
1473
    {
1474
        $query = $this->dbHandler->createDeleteQuery();
1475
        $query->deleteFrom(
1476
            $this->dbHandler->quoteTable('ezcontentobject_attribute')
1477
        )->where(
1478
            $query->expr->eq(
1479
                $this->dbHandler->quoteColumn('id'),
1480
                $query->bindValue($fieldId, null, \PDO::PARAM_INT)
1481
            )
1482
        );
1483
1484
        $query->prepare()->execute();
1485
    }
1486
1487
    /**
1488
     * Deletes all fields of $contentId in all versions.
1489
     * If $versionNo is set only fields for that version are deleted.
1490
     *
1491
     * @param int $contentId
1492
     * @param int|null $versionNo
1493
     */
1494
    public function deleteFields($contentId, $versionNo = null)
1495
    {
1496
        $query = $this->dbHandler->createDeleteQuery();
1497
        $query->deleteFrom('ezcontentobject_attribute')
1498
            ->where(
1499
                $query->expr->eq(
1500
                    $this->dbHandler->quoteColumn('contentobject_id'),
1501
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1502
                )
1503
            );
1504
1505
        if (isset($versionNo)) {
1506
            $query->where(
1507
                $query->expr->eq(
1508
                    $this->dbHandler->quoteColumn('version'),
1509
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1510
                )
1511
            );
1512
        }
1513
1514
        $query->prepare()->execute();
1515
    }
1516
1517
    /**
1518
     * Deletes all versions of $contentId.
1519
     * If $versionNo is set only that version is deleted.
1520
     *
1521
     * @param int $contentId
1522
     * @param int|null $versionNo
1523
     */
1524
    public function deleteVersions($contentId, $versionNo = null)
1525
    {
1526
        $query = $this->dbHandler->createDeleteQuery();
1527
        $query->deleteFrom('ezcontentobject_version')
1528
            ->where(
1529
                $query->expr->eq(
1530
                    $this->dbHandler->quoteColumn('contentobject_id'),
1531
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1532
                )
1533
            );
1534
1535
        if (isset($versionNo)) {
1536
            $query->where(
1537
                $query->expr->eq(
1538
                    $this->dbHandler->quoteColumn('version'),
1539
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1540
                )
1541
            );
1542
        }
1543
1544
        $query->prepare()->execute();
1545
    }
1546
1547
    /**
1548
     * Deletes all names of $contentId.
1549
     * If $versionNo is set only names for that version are deleted.
1550
     *
1551
     * @param int $contentId
1552
     * @param int|null $versionNo
1553
     */
1554
    public function deleteNames($contentId, $versionNo = null)
1555
    {
1556
        $query = $this->dbHandler->createDeleteQuery();
1557
        $query->deleteFrom('ezcontentobject_name')
1558
            ->where(
1559
                $query->expr->eq(
1560
                    $this->dbHandler->quoteColumn('contentobject_id'),
1561
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1562
                )
1563
            );
1564
1565
        if (isset($versionNo)) {
1566
            $query->where(
1567
                $query->expr->eq(
1568
                    $this->dbHandler->quoteColumn('content_version'),
1569
                    $query->bindValue($versionNo, null, \PDO::PARAM_INT)
1570
                )
1571
            );
1572
        }
1573
1574
        $query->prepare()->execute();
1575
    }
1576
1577
    /**
1578
     * Sets the name for Content $contentId in version $version to $name in $language.
1579
     *
1580
     * @param int $contentId
1581
     * @param int $version
1582
     * @param string $name
1583
     * @param string $language
1584
     */
1585
    public function setName($contentId, $version, $name, $language)
1586
    {
1587
        $language = $this->languageHandler->loadByLanguageCode($language);
1588
1589
        // Is it an insert or an update ?
1590
        $qSelect = $this->dbHandler->createSelectQuery();
1591
        $qSelect
1592
            ->select(
1593
                $qSelect->alias($qSelect->expr->count('*'), 'count')
1594
            )
1595
            ->from($this->dbHandler->quoteTable('ezcontentobject_name'))
1596
            ->where(
1597
                $qSelect->expr->lAnd(
1598
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $qSelect->bindValue($contentId)),
1599
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_version'), $qSelect->bindValue($version)),
1600
                    $qSelect->expr->eq($this->dbHandler->quoteColumn('content_translation'), $qSelect->bindValue($language->languageCode))
1601
                )
1602
            );
1603
        $stmt = $qSelect->prepare();
1604
        $stmt->execute();
1605
        $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1606
1607
        $insert = $res[0]['count'] == 0;
1608
        if ($insert) {
1609
            $q = $this->dbHandler->createInsertQuery();
1610
            $q->insertInto($this->dbHandler->quoteTable('ezcontentobject_name'));
1611
        } else {
1612
            $q = $this->dbHandler->createUpdateQuery();
1613
            $q->update($this->dbHandler->quoteTable('ezcontentobject_name'))
1614
                ->where(
1615
                    $q->expr->lAnd(
1616
                        $q->expr->eq($this->dbHandler->quoteColumn('contentobject_id'), $q->bindValue($contentId)),
1617
                        $q->expr->eq($this->dbHandler->quoteColumn('content_version'), $q->bindValue($version)),
1618
                        $q->expr->eq($this->dbHandler->quoteColumn('content_translation'), $q->bindValue($language->languageCode))
1619
                    )
1620
                );
1621
        }
1622
1623
        $q->set(
1624
            $this->dbHandler->quoteColumn('contentobject_id'),
1625
            $q->bindValue($contentId, null, \PDO::PARAM_INT)
1626
        )->set(
1627
            $this->dbHandler->quoteColumn('content_version'),
1628
            $q->bindValue($version, null, \PDO::PARAM_INT)
1629
        )->set(
1630
            $this->dbHandler->quoteColumn('language_id'),
1631
            '(' . $this->getLanguageQuery()->getQuery() . ')'
1632
        )->set(
1633
            $this->dbHandler->quoteColumn('content_translation'),
1634
            $q->bindValue($language->languageCode)
1635
        )->set(
1636
            $this->dbHandler->quoteColumn('real_translation'),
1637
            $q->bindValue($language->languageCode)
1638
        )->set(
1639
            $this->dbHandler->quoteColumn('name'),
1640
            $q->bindValue($name)
1641
        );
1642
        $q->bindValue($language->id, ':languageId', \PDO::PARAM_INT);
1643
        $q->bindValue($contentId, ':contentId', \PDO::PARAM_INT);
1644
        $q->prepare()->execute();
1645
    }
1646
1647
    /**
1648
     * Returns a language sub select query for setName.
1649
     *
1650
     * Return sub select query which gets proper language mask for alwaysAvailable Content.
1651
     *
1652
     * @return \eZ\Publish\Core\Persistence\Database\SelectQuery
1653
     */
1654
    private function getLanguageQuery()
1655
    {
1656
        $languageQuery = $this->dbHandler->createSelectQuery();
1657
        $languageQuery
1658
            ->select(
1659
                $languageQuery->expr->searchedCase(
1660
                    [
1661
                        $languageQuery->expr->lAnd(
1662
                            $languageQuery->expr->eq(
1663
                                $this->dbHandler->quoteColumn('initial_language_id'),
1664
                                ':languageId'
1665
                            ),
1666
                            // wrap bitwise check into another "neq" to provide cross-DBMS compatibility
1667
                            $languageQuery->expr->neq(
1668
                                $languageQuery->expr->bitAnd(
1669
                                    $this->dbHandler->quoteColumn('language_mask'),
1670
                                    ':languageId'
1671
                                ),
1672
                                0
1673
                            )
1674
                        ),
1675
                        $languageQuery->expr->bitOr(
1676
                            ':languageId',
1677
                            1
1678
                        ),
1679
                    ],
1680
                    ':languageId'
1681
                )
1682
            )
1683
            ->from('ezcontentobject')
1684
            ->where(
1685
                $languageQuery->expr->eq(
1686
                    'id',
1687
                    ':contentId'
1688
                )
1689
            );
1690
1691
        return $languageQuery;
1692
    }
1693
1694
    /**
1695
     * Deletes the actual content object referred to by $contentId.
1696
     *
1697
     * @param int $contentId
1698
     */
1699
    public function deleteContent($contentId)
1700
    {
1701
        $query = $this->dbHandler->createDeleteQuery();
1702
        $query->deleteFrom('ezcontentobject')
1703
            ->where(
1704
                $query->expr->eq(
1705
                    $this->dbHandler->quoteColumn('id'),
1706
                    $query->bindValue($contentId, null, \PDO::PARAM_INT)
1707
                )
1708
            );
1709
1710
        $query->prepare()->execute();
1711
    }
1712
1713
    /**
1714
     * Loads relations from $contentId to published content, optionally only from $contentVersionNo.
1715
     *
1716
     * $relationType can also be filtered.
1717
     *
1718
     * @param int $contentId
1719
     * @param int $contentVersionNo
1720
     * @param int $relationType
1721
     *
1722
     * @return string[][] array of relation data
1723
     */
1724
    public function loadRelations($contentId, $contentVersionNo = null, $relationType = null)
1725
    {
1726
        $query = $this->queryBuilder->createRelationFindQuery();
1727
        $query->innerJoin(
1728
            $query->alias(
1729
                $this->dbHandler->quoteTable('ezcontentobject'),
1730
                'ezcontentobject_to'
1731
            ),
1732
            $query->expr->lAnd(
1733
                $query->expr->eq(
1734
                    $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1735
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject_to')
1736
                ),
1737
                $query->expr->eq(
1738
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject_to'),
1739
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1740
                )
1741
            )
1742
        )->where(
1743
            $query->expr->eq(
1744
                $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link'),
1745
                $query->bindValue($contentId, null, \PDO::PARAM_INT)
1746
            )
1747
        );
1748
1749
        // source version number
1750
        if (isset($contentVersionNo)) {
1751
            $query->where(
1752
                $query->expr->eq(
1753
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link'),
1754
                    $query->bindValue($contentVersionNo, null, \PDO::PARAM_INT)
1755
                )
1756
            );
1757
        } else { // from published version only
1758
            $query->from(
1759
                $this->dbHandler->quoteTable('ezcontentobject')
1760
            )->where(
1761
                $query->expr->lAnd(
1762
                    $query->expr->eq(
1763
                        $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1764
                        $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1765
                    ),
1766
                    $query->expr->eq(
1767
                        $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1768
                        $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1769
                    )
1770
                )
1771
            );
1772
        }
1773
1774
        // relation type
1775 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...
1776
            $query->where(
1777
                $query->expr->gt(
1778
                    $query->expr->bitAnd(
1779
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1780
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1781
                    ),
1782
                    0
1783
                )
1784
            );
1785
        }
1786
1787
        $statement = $query->prepare();
1788
        $statement->execute();
1789
1790
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1791
    }
1792
1793
    /**
1794
     * Loads data that related to $toContentId.
1795
     *
1796
     * @param int $toContentId
1797
     * @param int $relationType
1798
     *
1799
     * @return mixed[][] Content data, array structured like {@see \eZ\Publish\Core\Persistence\Legacy\Content\Gateway::load()}
1800
     */
1801
    public function loadReverseRelations($toContentId, $relationType = null)
1802
    {
1803
        $query = $this->queryBuilder->createRelationFindQuery();
1804
        $query->where(
1805
            $query->expr->eq(
1806
                $this->dbHandler->quoteColumn('to_contentobject_id', 'ezcontentobject_link'),
1807
                $query->bindValue($toContentId, null, \PDO::PARAM_INT)
1808
            )
1809
        );
1810
1811
        // ezcontentobject join
1812
        $query->from(
1813
            $this->dbHandler->quoteTable('ezcontentobject')
1814
        )->where(
1815
            $query->expr->lAnd(
1816
                $query->expr->eq(
1817
                    $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
1818
                    $this->dbHandler->quoteColumn('from_contentobject_id', 'ezcontentobject_link')
1819
                ),
1820
                $query->expr->eq(
1821
                    $this->dbHandler->quoteColumn('current_version', 'ezcontentobject'),
1822
                    $this->dbHandler->quoteColumn('from_contentobject_version', 'ezcontentobject_link')
1823
                ),
1824
                $query->expr->eq(
1825
                    $this->dbHandler->quoteColumn('status', 'ezcontentobject'),
1826
                    $query->bindValue(1, null, \PDO::PARAM_INT)
1827
                )
1828
            )
1829
        );
1830
1831
        // relation type
1832 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...
1833
            $query->where(
1834
                $query->expr->gt(
1835
                    $query->expr->bitAnd(
1836
                        $this->dbHandler->quoteColumn('relation_type', 'ezcontentobject_link'),
1837
                        $query->bindValue($relationType, null, \PDO::PARAM_INT)
1838
                    ),
1839
                    0
1840
                )
1841
            );
1842
        }
1843
1844
        $statement = $query->prepare();
1845
1846
        $statement->execute();
1847
1848
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1849
    }
1850
1851
    /**
1852
     * Inserts a new relation database record.
1853
     *
1854
     * @param \eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct $createStruct
1855
     *
1856
     * @return int ID the inserted ID
1857
     */
1858 View Code Duplication
    public function insertRelation(RelationCreateStruct $createStruct)
1859
    {
1860
        $q = $this->dbHandler->createInsertQuery();
1861
        $q->insertInto(
1862
            $this->dbHandler->quoteTable('ezcontentobject_link')
1863
        )->set(
1864
            $this->dbHandler->quoteColumn('id'),
1865
            $this->dbHandler->getAutoIncrementValue('ezcontentobject_link', 'id')
1866
        )->set(
1867
            $this->dbHandler->quoteColumn('contentclassattribute_id'),
1868
            $q->bindValue((int)$createStruct->sourceFieldDefinitionId, null, \PDO::PARAM_INT)
1869
        )->set(
1870
            $this->dbHandler->quoteColumn('from_contentobject_id'),
1871
            $q->bindValue($createStruct->sourceContentId, null, \PDO::PARAM_INT)
1872
        )->set(
1873
            $this->dbHandler->quoteColumn('from_contentobject_version'),
1874
            $q->bindValue($createStruct->sourceContentVersionNo, null, \PDO::PARAM_INT)
1875
        )->set(
1876
            $this->dbHandler->quoteColumn('relation_type'),
1877
            $q->bindValue($createStruct->type, null, \PDO::PARAM_INT)
1878
        )->set(
1879
            $this->dbHandler->quoteColumn('to_contentobject_id'),
1880
            $q->bindValue($createStruct->destinationContentId, null, \PDO::PARAM_INT)
1881
        );
1882
1883
        $q->prepare()->execute();
1884
1885
        return $this->dbHandler->lastInsertId(
1886
            $this->dbHandler->getSequenceName('ezcontentobject_link', 'id')
1887
        );
1888
    }
1889
1890
    /**
1891
     * Deletes the relation with the given $relationId.
1892
     *
1893
     * @param int $relationId
1894
     * @param int $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
1895
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
1896
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
1897
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
1898
     */
1899
    public function deleteRelation($relationId, $type)
1900
    {
1901
        // Legacy Storage stores COMMON, LINK and EMBED types using bitmask, therefore first load
1902
        // existing relation type by given $relationId for comparison
1903
        /** @var $query \eZ\Publish\Core\Persistence\Database\SelectQuery */
1904
        $query = $this->dbHandler->createSelectQuery();
1905
        $query->select(
1906
            $this->dbHandler->quoteColumn('relation_type')
1907
        )->from(
1908
            $this->dbHandler->quoteTable('ezcontentobject_link')
1909
        )->where(
1910
            $query->expr->eq(
1911
                $this->dbHandler->quoteColumn('id'),
1912
                $query->bindValue($relationId, null, \PDO::PARAM_INT)
1913
            )
1914
        );
1915
1916
        $statement = $query->prepare();
1917
        $statement->execute();
1918
        $loadedRelationType = $statement->fetchColumn();
1919
1920
        if (!$loadedRelationType) {
1921
            return;
1922
        }
1923
1924
        // If relation type matches then delete
1925
        if ($loadedRelationType == $type) {
1926
            /** @var $query \eZ\Publish\Core\Persistence\Database\DeleteQuery */
1927
            $query = $this->dbHandler->createDeleteQuery();
1928
            $query->deleteFrom(
1929
                'ezcontentobject_link'
1930
            )->where(
1931
                $query->expr->eq(
1932
                    $this->dbHandler->quoteColumn('id'),
1933
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
1934
                )
1935
            );
1936
1937
            $query->prepare()->execute();
1938
        } elseif ($loadedRelationType & $type) { // If relation type is composite update bitmask
1939
            /** @var $query \eZ\Publish\Core\Persistence\Database\UpdateQuery */
1940
            $query = $this->dbHandler->createUpdateQuery();
1941
            $query->update(
1942
                $this->dbHandler->quoteTable('ezcontentobject_link')
1943
            )->set(
1944
                $this->dbHandler->quoteColumn('relation_type'),
1945
                $query->expr->bitAnd(
1946
                    $this->dbHandler->quoteColumn('relation_type'),
1947
                    $query->bindValue(~$type, null, \PDO::PARAM_INT)
1948
                )
1949
            )->where(
1950
                $query->expr->eq(
1951
                    $this->dbHandler->quoteColumn('id'),
1952
                    $query->bindValue($relationId, null, \PDO::PARAM_INT)
1953
                )
1954
            );
1955
1956
            $query->prepare()->execute();
1957
        } else {
1958
            // No match, do nothing
1959
        }
1960
    }
1961
1962
    /**
1963
     * Returns all Content IDs for a given $contentTypeId.
1964
     *
1965
     * @param int $contentTypeId
1966
     *
1967
     * @return int[]
1968
     */
1969
    public function getContentIdsByContentTypeId($contentTypeId)
1970
    {
1971
        $query = $this->dbHandler->createSelectQuery();
1972
        $query
1973
            ->select($this->dbHandler->quoteColumn('id'))
1974
            ->from($this->dbHandler->quoteTable('ezcontentobject'))
1975
            ->where(
1976
                $query->expr->eq(
1977
                    $this->dbHandler->quoteColumn('contentclass_id'),
1978
                    $query->bindValue($contentTypeId, null, PDO::PARAM_INT)
1979
                )
1980
            );
1981
1982
        $statement = $query->prepare();
1983
        $statement->execute();
1984
1985
        return $statement->fetchAll(PDO::FETCH_COLUMN);
1986
    }
1987
1988
    /**
1989
     * Load name data for set of content id's and corresponding version number.
1990
     *
1991
     * @param array[] $rows array of hashes with 'id' and 'version' to load names for
1992
     *
1993
     * @return array
1994
     */
1995
    public function loadVersionedNameData($rows)
1996
    {
1997
        $query = $this->queryBuilder->createNamesQuery();
1998
        $conditions = array();
1999
        foreach ($rows as $row) {
2000
            $conditions[] = $query->expr->lAnd(
2001
                $query->expr->eq(
2002
                    $this->dbHandler->quoteColumn('contentobject_id'),
2003
                    $query->bindValue($row['id'], null, \PDO::PARAM_INT)
2004
                ),
2005
                $query->expr->eq(
2006
                    $this->dbHandler->quoteColumn('content_version'),
2007
                    $query->bindValue($row['version'], null, \PDO::PARAM_INT)
2008
                )
2009
            );
2010
        }
2011
2012
        $query->where($query->expr->lOr($conditions));
2013
        $stmt = $query->prepare();
2014
        $stmt->execute();
2015
2016
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
2017
    }
2018
2019
    /**
2020
     * Batch method for copying all relation meta data for copied Content object.
2021
     *
2022
     * {@inheritdoc}
2023
     *
2024
     * @param int $originalContentId
2025
     * @param int $copiedContentId
2026
     * @param int|null $versionNo If specified only copy for a given version number, otherwise all.
2027
     */
2028
    public function copyRelations($originalContentId, $copiedContentId, $versionNo = null)
2029
    {
2030
        // Given we can retain all columns, we just create copies with new `from_contentobject_id` using INSERT INTO SELECT
2031
        $sql = 'INSERT INTO ezcontentobject_link ( contentclassattribute_id, from_contentobject_id, from_contentobject_version, relation_type, to_contentobject_id )
2032
                SELECT  L2.contentclassattribute_id, :copied_id, L2.from_contentobject_version, L2.relation_type, L2.to_contentobject_id
2033
                FROM    ezcontentobject_link AS L2
2034
                WHERE   L2.from_contentobject_id = :original_id';
2035
2036
        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...
2037
            $stmt = $this->connection->prepare($sql . ' AND L2.from_contentobject_version = :version');
2038
            $stmt->bindValue('version', $versionNo, PDO::PARAM_INT);
2039
        } else {
2040
            $stmt = $this->connection->prepare($sql);
2041
        }
2042
2043
        $stmt->bindValue('original_id', $originalContentId, PDO::PARAM_INT);
2044
        $stmt->bindValue('copied_id', $copiedContentId, PDO::PARAM_INT);
2045
2046
        $stmt->execute();
2047
    }
2048
2049
    /**
2050
     * Remove the specified translation from the Content Object Version.
2051
     *
2052
     * @param int $contentId
2053
     * @param string $languageCode language code of the translation
2054
     * @throws \Doctrine\DBAL\DBALException
2055
     */
2056 View Code Duplication
    public function deleteTranslationFromContent($contentId, $languageCode)
2057
    {
2058
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2059
2060
        $this->connection->beginTransaction();
2061
        try {
2062
            $this->deleteTranslationFromContentVersions($contentId, $language->id);
2063
            $this->deleteTranslationFromContentNames($contentId, $languageCode);
2064
            $this->deleteTranslationFromContentObject($contentId, $language->id);
2065
2066
            $this->connection->commit();
2067
        } catch (DBALException $e) {
2068
            $this->connection->rollBack();
2069
            throw $e;
2070
        }
2071
    }
2072
2073
    /**
2074
     * Delete Content fields (attributes) for the given Translation.
2075
     * If $versionNo is given, fields for that Version only will be deleted.
2076
     *
2077
     * @param string $languageCode
2078
     * @param int $contentId
2079
     * @param int $versionNo (optional) filter by versionNo
2080
     */
2081 View Code Duplication
    public function deleteTranslatedFields($languageCode, $contentId, $versionNo = null)
2082
    {
2083
        $query = $this->connection->createQueryBuilder();
2084
        $query
2085
            ->delete('ezcontentobject_attribute')
2086
            ->where('contentobject_id = :contentId')
2087
            ->andWhere('language_code = :languageCode')
2088
            ->setParameters(
2089
                [
2090
                    ':contentId' => $contentId,
2091
                    ':languageCode' => $languageCode,
2092
                ]
2093
            )
2094
        ;
2095
2096
        if (null !== $versionNo) {
2097
            $query
2098
                ->andWhere('version = :versionNo')
2099
                ->setParameter(':versionNo', $versionNo)
2100
            ;
2101
        }
2102
2103
        $query->execute();
2104
    }
2105
2106
    /**
2107
     * Delete the specified Translation from the given Version.
2108
     *
2109
     * @param int $contentId
2110
     * @param int $versionNo
2111
     * @param string $languageCode
2112
     * @throws \Doctrine\DBAL\DBALException
2113
     */
2114 View Code Duplication
    public function deleteTranslationFromVersion($contentId, $versionNo, $languageCode)
2115
    {
2116
        $language = $this->languageHandler->loadByLanguageCode($languageCode);
2117
2118
        $this->connection->beginTransaction();
2119
        try {
2120
            $this->deleteTranslationFromContentVersions($contentId, $language->id, $versionNo);
2121
            $this->deleteTranslationFromContentNames($contentId, $languageCode, $versionNo);
2122
2123
            $this->connection->commit();
2124
        } catch (DBALException $e) {
2125
            $this->connection->rollBack();
2126
            throw $e;
2127
        }
2128
    }
2129
2130
    /**
2131
     * Delete translation from the ezcontentobject_name table.
2132
     *
2133
     * @param int $contentId
2134
     * @param string $languageCode
2135
     * @param int $versionNo optional, if specified, apply to this Version only.
2136
     */
2137 View Code Duplication
    private function deleteTranslationFromContentNames($contentId, $languageCode, $versionNo = null)
2138
    {
2139
        $query = $this->connection->createQueryBuilder();
2140
        $query
2141
            ->delete('ezcontentobject_name')
2142
            ->where('contentobject_id=:contentId')
2143
            ->andWhere('real_translation=:languageCode')
2144
            ->setParameters(
2145
                [
2146
                    ':languageCode' => $languageCode,
2147
                    ':contentId' => $contentId,
2148
                ]
2149
            )
2150
        ;
2151
2152
        if (null !== $versionNo) {
2153
            $query
2154
                ->andWhere('content_version = :versionNo')
2155
                ->setParameter(':versionNo', $versionNo)
2156
            ;
2157
        }
2158
2159
        $query->execute();
2160
    }
2161
2162
    /**
2163
     * Remove language from language_mask of ezcontentobject.
2164
     *
2165
     * @param int $contentId
2166
     * @param int $languageId
2167
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2168
     */
2169
    private function deleteTranslationFromContentObject($contentId, $languageId)
2170
    {
2171
        $query = $this->connection->createQueryBuilder();
2172
        $query->update('ezcontentobject')
2173
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2174
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2175
            ->set('modified', ':now')
2176
            ->where('id = :contentId')
2177
            ->andWhere(
2178
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2179
                $query->expr()->andX(
2180
                    'language_mask & ~ ' . $languageId . ' <> 0',
2181
                    'language_mask & ~ ' . $languageId . ' <> 1'
2182
                )
2183
            )
2184
            ->setParameter(':now', time())
2185
            ->setParameter(':contentId', $contentId)
2186
        ;
2187
2188
        $rowCount = $query->execute();
2189
2190
        // no rows updated means that most likely somehow it was the last remaining translation
2191
        if ($rowCount === 0) {
2192
            throw new BadStateException(
2193
                '$languageCode',
2194
                'Specified translation is the only one Content Object Version has'
2195
            );
2196
        }
2197
    }
2198
2199
    /**
2200
     * Remove language from language_mask of ezcontentobject_version and update initialLanguageId
2201
     * if it matches the removed one.
2202
     *
2203
     * @param int $contentId
2204
     * @param int $languageId
2205
     * @param int $versionNo optional, if specified, apply to this Version only.
2206
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException
2207
     */
2208
    private function deleteTranslationFromContentVersions($contentId, $languageId, $versionNo = null)
2209
    {
2210
        $query = $this->connection->createQueryBuilder();
2211
        $query->update('ezcontentobject_version')
2212
            // parameter for bitwise operation has to be placed verbatim (w/o binding) for this to work cross-DBMS
2213
            ->set('language_mask', 'language_mask & ~ ' . $languageId)
2214
            ->set('modified', ':now')
2215
            // update initial_language_id only if it matches removed translation languageId
2216
            ->set(
2217
                'initial_language_id',
2218
                'CASE WHEN initial_language_id = :languageId ' .
2219
                'THEN (SELECT initial_language_id AS main_language_id FROM ezcontentobject c WHERE c.id = :contentId) ' .
2220
                'ELSE initial_language_id END'
2221
            )
2222
            ->where('contentobject_id = :contentId')
2223
            ->andWhere(
2224
            // make sure removed translation is not the last one (incl. alwaysAvailable)
2225
                $query->expr()->andX(
2226
                    'language_mask & ~ ' . $languageId . ' <> 0',
2227
                    'language_mask & ~ ' . $languageId . ' <> 1'
2228
                )
2229
            )
2230
            ->setParameter(':now', time())
2231
            ->setParameter(':contentId', $contentId)
2232
            ->setParameter(':languageId', $languageId)
2233
        ;
2234
2235
        if (null !== $versionNo) {
2236
            $query
2237
                ->andWhere('version = :versionNo')
2238
                ->setParameter(':versionNo', $versionNo)
2239
            ;
2240
        }
2241
2242
        $rowCount = $query->execute();
2243
2244
        // no rows updated means that most likely somehow it was the last remaining translation
2245
        if ($rowCount === 0) {
2246
            throw new BadStateException(
2247
                '$languageCode',
2248
                'Specified translation is the only one Content Object Version has'
2249
            );
2250
        }
2251
    }
2252
}
2253