Completed
Push — master ( 7b418d...9c4396 )
by
unknown
30:11 queued 11:26
created

Handler::copyTranslations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
c 0
b 0
f 0
cc 3
nc 3
nop 2
rs 9.296
1
<?php
2
3
/**
4
 * File containing the Content Handler 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;
10
11
use Exception;
12
use eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
13
use eZ\Publish\SPI\Persistence\Content\Field;
14
use eZ\Publish\SPI\Persistence\Content\Handler as BaseContentHandler;
15
use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
16
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter;
17
use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway as UrlAliasGateway;
18
use eZ\Publish\SPI\Persistence\Content;
19
use eZ\Publish\SPI\Persistence\Content\CreateStruct;
20
use eZ\Publish\SPI\Persistence\Content\UpdateStruct;
21
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct;
22
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
23
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
24
use eZ\Publish\Core\Base\Exceptions\NotFoundException as NotFound;
25
use Psr\Log\LoggerInterface;
26
use Psr\Log\NullLogger;
27
28
/**
29
 * The Content Handler stores Content and ContentType objects.
30
 */
31
class Handler implements BaseContentHandler
32
{
33
    /**
34
     * Content gateway.
35
     *
36
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway
37
     */
38
    protected $contentGateway;
39
40
    /**
41
     * Location gateway.
42
     *
43
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway
44
     */
45
    protected $locationGateway;
46
47
    /**
48
     * Mapper.
49
     *
50
     * @var Mapper
51
     */
52
    protected $mapper;
53
54
    /**
55
     * FieldHandler.
56
     *
57
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\FieldHandler
58
     */
59
    protected $fieldHandler;
60
61
    /**
62
     * URL slug converter.
63
     *
64
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter
65
     */
66
    protected $slugConverter;
67
68
    /**
69
     * UrlAlias gateway.
70
     *
71
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway
72
     */
73
    protected $urlAliasGateway;
74
75
    /**
76
     * ContentType handler.
77
     *
78
     * @var \eZ\Publish\SPI\Persistence\Content\Type\Handler
79
     */
80
    protected $contentTypeHandler;
81
82
    /**
83
     * Tree handler.
84
     *
85
     * @var \eZ\Publish\Core\Persistence\Legacy\Content\TreeHandler
86
     */
87
    protected $treeHandler;
88
89
    /**
90
     * @var \Psr\Log\LoggerInterface
91
     */
92
    private $logger;
93
94
    /**
95
     * Creates a new content handler.
96
     *
97
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway $contentGateway
98
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway
99
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\Mapper $mapper
100
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\FieldHandler $fieldHandler
101
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter
102
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $urlAliasGateway
103
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
104
     * @param \eZ\Publish\Core\Persistence\Legacy\Content\TreeHandler $treeHandler
105
     * @param \Psr\Log\LoggerInterface|null $logger
106
     */
107
    public function __construct(
108
        Gateway $contentGateway,
109
        LocationGateway $locationGateway,
110
        Mapper $mapper,
111
        FieldHandler $fieldHandler,
112
        SlugConverter $slugConverter,
113
        UrlAliasGateway $urlAliasGateway,
114
        ContentTypeHandler $contentTypeHandler,
115
        TreeHandler $treeHandler,
116
        LoggerInterface $logger = null
117
    ) {
118
        $this->contentGateway = $contentGateway;
119
        $this->locationGateway = $locationGateway;
120
        $this->mapper = $mapper;
121
        $this->fieldHandler = $fieldHandler;
122
        $this->slugConverter = $slugConverter;
123
        $this->urlAliasGateway = $urlAliasGateway;
124
        $this->contentTypeHandler = $contentTypeHandler;
125
        $this->treeHandler = $treeHandler;
126
        $this->logger = null !== $logger ? $logger : new NullLogger();
127
    }
128
129
    /**
130
     * Creates a new Content entity in the storage engine.
131
     *
132
     * The values contained inside the $content will form the basis of stored
133
     * entity.
134
     *
135
     * Will contain always a complete list of fields.
136
     *
137
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct Content creation struct.
138
     *
139
     * @return \eZ\Publish\SPI\Persistence\Content Content value object
140
     */
141
    public function create(CreateStruct $struct)
142
    {
143
        return $this->internalCreate($struct);
144
    }
145
146
    /**
147
     * Creates a new Content entity in the storage engine.
148
     *
149
     * The values contained inside the $content will form the basis of stored
150
     * entity.
151
     *
152
     * Will contain always a complete list of fields.
153
     *
154
     * @param \eZ\Publish\SPI\Persistence\Content\CreateStruct $struct Content creation struct.
155
     * @param mixed $versionNo Used by self::copy() to maintain version numbers
156
     *
157
     * @return \eZ\Publish\SPI\Persistence\Content Content value object
158
     */
159
    protected function internalCreate(CreateStruct $struct, $versionNo = 1)
160
    {
161
        $content = new Content();
162
163
        $content->fields = $struct->fields;
164
        $content->versionInfo = $this->mapper->createVersionInfoFromCreateStruct($struct, $versionNo);
165
166
        $content->versionInfo->contentInfo->id = $this->contentGateway->insertContentObject($struct, $versionNo);
167
        $content->versionInfo->id = $this->contentGateway->insertVersion(
168
            $content->versionInfo,
169
            $struct->fields
170
        );
171
172
        $contentType = $this->contentTypeHandler->load($struct->typeId);
173
        $this->fieldHandler->createNewFields($content, $contentType);
174
175
        // Create node assignments
176
        foreach ($struct->locations as $location) {
177
            $location->contentId = $content->versionInfo->contentInfo->id;
178
            $location->contentVersion = $content->versionInfo->versionNo;
179
            $this->locationGateway->createNodeAssignment(
180
                $location,
181
                $location->parentId,
182
                LocationGateway::NODE_ASSIGNMENT_OP_CODE_CREATE
183
            );
184
        }
185
186
        // Create names
187 View Code Duplication
        foreach ($content->versionInfo->names as $language => $name) {
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...
188
            $this->contentGateway->setName(
189
                $content->versionInfo->contentInfo->id,
190
                $content->versionInfo->versionNo,
191
                $name,
192
                $language
193
            );
194
        }
195
196
        return $content;
197
    }
198
199
    /**
200
     * Performs the publishing operations required to set the version identified by $updateStruct->versionNo and
201
     * $updateStruct->id as the published one.
202
     *
203
     * The publish procedure will:
204
     * - Create location nodes based on the node assignments
205
     * - Update the content object using the provided metadata update struct
206
     * - Update the node assignments
207
     * - Update location nodes of the content with the new published version
208
     * - Set content and version status to published
209
     *
210
     * @param int $contentId
211
     * @param int $versionNo
212
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $metaDataUpdateStruct
213
     *
214
     * @return \eZ\Publish\SPI\Persistence\Content The published Content
215
     */
216
    public function publish($contentId, $versionNo, MetadataUpdateStruct $metaDataUpdateStruct)
217
    {
218
        // Archive currently published version
219
        $versionInfo = $this->loadVersionInfo($contentId, $versionNo);
220
        if ($versionInfo->contentInfo->currentVersionNo != $versionNo) {
221
            $this->setStatus(
222
                $contentId,
223
                VersionInfo::STATUS_ARCHIVED,
224
                $versionInfo->contentInfo->currentVersionNo
225
            );
226
        }
227
228
        // Set always available name for the content
229
        $metaDataUpdateStruct->name = $versionInfo->names[$versionInfo->contentInfo->mainLanguageCode];
230
231
        $this->contentGateway->updateContent($contentId, $metaDataUpdateStruct, $versionInfo);
232
        $this->locationGateway->createLocationsFromNodeAssignments(
233
            $contentId,
234
            $versionNo
235
        );
236
237
        $this->locationGateway->updateLocationsContentVersionNo($contentId, $versionNo);
238
        $this->setStatus($contentId, VersionInfo::STATUS_PUBLISHED, $versionNo);
239
240
        return $this->load($contentId, $versionNo);
241
    }
242
243
    /**
244
     * Creates a new draft version from $contentId in $version.
245
     *
246
     * Copies all fields from $contentId in $srcVersion and creates a new
247
     * version of the referred Content from it.
248
     *
249
     * Note: When creating a new draft in the old admin interface there will
250
     * also be an entry in the `eznode_assignment` created for the draft. This
251
     * is ignored in this implementation.
252
     *
253
     * @param mixed $contentId
254
     * @param mixed $srcVersion
255
     * @param mixed $userId
256
     *
257
     * @return \eZ\Publish\SPI\Persistence\Content
258
     */
259
    public function createDraftFromVersion($contentId, $srcVersion, $userId)
260
    {
261
        $content = $this->load($contentId, $srcVersion);
262
263
        // Create new version
264
        $content->versionInfo = $this->mapper->createVersionInfoForContent(
265
            $content,
266
            $this->contentGateway->getLastVersionNumber($contentId) + 1,
267
            $userId
268
        );
269
        $content->versionInfo->id = $this->contentGateway->insertVersion(
270
            $content->versionInfo,
271
            $content->fields
272
        );
273
274
        // Clone fields from previous version and append them to the new one
275
        $this->fieldHandler->createExistingFieldsInNewVersion($content);
276
277
        // Create relations for new version
278
        $relations = $this->contentGateway->loadRelations($contentId, $srcVersion);
279
        foreach ($relations as $relation) {
280
            $this->contentGateway->insertRelation(
281
                new RelationCreateStruct(
282
                    array(
283
                        'sourceContentId' => $contentId,
284
                        'sourceContentVersionNo' => $content->versionInfo->versionNo,
285
                        'sourceFieldDefinitionId' => $relation['ezcontentobject_link_contentclassattribute_id'],
286
                        'destinationContentId' => $relation['ezcontentobject_link_to_contentobject_id'],
287
                        'type' => (int)$relation['ezcontentobject_link_relation_type'],
288
                    )
289
                )
290
            );
291
        }
292
293
        // Create content names for new version
294
        foreach ($content->versionInfo->names as $language => $name) {
295
            $this->contentGateway->setName(
296
                $contentId,
297
                $content->versionInfo->versionNo,
298
                $name,
299
                $language
300
            );
301
        }
302
303
        return $content;
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function load($id, $version = null, array $translations = null)
310
    {
311
        $rows = $this->contentGateway->load($id, $version, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 309 can also be of type array; however, eZ\Publish\Core\Persiste...Content\Gateway::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
312
313
        if (empty($rows)) {
314
            throw new NotFound('content', "contentId: $id, versionNo: $version");
315
        }
316
317
        $contentObjects = $this->mapper->extractContentFromRows(
318
            $rows,
319
            $this->contentGateway->loadVersionedNameData([[
320
                'id' => $id,
321
                'version' => $rows[0]['ezcontentobject_version_version'],
322
            ]])
323
        );
324
        $content = $contentObjects[0];
325
        unset($rows, $contentObjects);
326
327
        $this->fieldHandler->loadExternalFieldData($content);
328
329
        return $content;
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    public function loadContentList(array $contentIds, array $translations = null): array
336
    {
337
        $rawList = $this->contentGateway->loadContentList($contentIds, $translations);
0 ignored issues
show
Bug introduced by
It seems like $translations defined by parameter $translations on line 335 can also be of type array; however, eZ\Publish\Core\Persiste...eway::loadContentList() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
338
        if (empty($rawList)) {
339
            return [];
340
        }
341
342
        $idVersionPairs = [];
343
        foreach ($rawList as $row) {
344
            // As there is only one version per id, set id as key to avoid duplicates
345
            $idVersionPairs[$row['ezcontentobject_id']] = [
346
                'id' => $row['ezcontentobject_id'],
347
                'version' => $row['ezcontentobject_version_version'],
348
            ];
349
        }
350
351
        // group name data per Content Id
352
        $nameData = $this->contentGateway->loadVersionedNameData(array_values($idVersionPairs));
353
        $contentItemNameData = [];
354
        foreach ($nameData as $nameDataRow) {
355
            $contentId = $nameDataRow['ezcontentobject_name_contentobject_id'];
356
            $contentItemNameData[$contentId][] = $nameDataRow;
357
        }
358
359
        // group rows per Content Id be able to ignore Content items with erroneous data
360
        $contentItemsRows = [];
361
        foreach ($rawList as $row) {
362
            $contentId = $row['ezcontentobject_id'];
363
            $contentItemsRows[$contentId][] = $row;
364
        }
365
        unset($rawList, $idVersionPairs);
366
367
        // try to extract Content from each Content data
368
        $contentItems = [];
369
        foreach ($contentItemsRows as $contentId => $contentItemsRow) {
370
            try {
371
                $contentList = $this->mapper->extractContentFromRows(
372
                    $contentItemsRow,
373
                    $contentItemNameData[$contentId]
374
                );
375
                $contentItems[$contentId] = $contentList[0];
376
            } catch (Exception $e) {
377
                $this->logger->warning(
378
                    sprintf(
379
                        '%s: Content %d not loaded: %s',
380
                        __METHOD__,
381
                        $contentId,
382
                        $e->getMessage()
383
                    )
384
                );
385
            }
386
        }
387
388
        // try to load External Storage data for each Content, ignore Content items for which it failed
389
        foreach ($contentItems as $contentId => $content) {
390
            try {
391
                $this->fieldHandler->loadExternalFieldData($content);
392
            } catch (Exception $e) {
393
                unset($contentItems[$contentId]);
394
                $this->logger->warning(
395
                    sprintf(
396
                        '%s: Content %d not loaded: %s',
397
                        __METHOD__,
398
                        $contentId,
399
                        $e->getMessage()
400
                    )
401
                );
402
            }
403
        }
404
405
        return $contentItems;
406
    }
407
408
    /**
409
     * Returns the metadata object for a content identified by $contentId.
410
     *
411
     * @param int|string $contentId
412
     *
413
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo
414
     */
415
    public function loadContentInfo($contentId)
416
    {
417
        return $this->treeHandler->loadContentInfo($contentId);
418
    }
419
420
    public function loadContentInfoList(array $contentIds)
421
    {
422
        $list = $this->mapper->extractContentInfoFromRows(
423
            $this->contentGateway->loadContentInfoList($contentIds)
424
        );
425
426
        $listByContentId = [];
427
        foreach ($list as $item) {
428
            $listByContentId[$item->id] = $item;
429
        }
430
431
        return $listByContentId;
432
    }
433
434
    /**
435
     * Returns the metadata object for a content identified by $remoteId.
436
     *
437
     * @param mixed $remoteId
438
     *
439
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo
440
     */
441
    public function loadContentInfoByRemoteId($remoteId)
442
    {
443
        return $this->mapper->extractContentInfoFromRow(
444
            $this->contentGateway->loadContentInfoByRemoteId($remoteId)
445
        );
446
    }
447
448
    /**
449
     * Returns the version object for a content/version identified by $contentId and $versionNo.
450
     *
451
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If version is not found
452
     *
453
     * @param int|string $contentId
454
     * @param int $versionNo Version number to load
455
     *
456
     * @return \eZ\Publish\SPI\Persistence\Content\VersionInfo
457
     */
458 View Code Duplication
    public function loadVersionInfo($contentId, $versionNo)
459
    {
460
        $rows = $this->contentGateway->loadVersionInfo($contentId, $versionNo);
461
        if (empty($rows)) {
462
            throw new NotFound('content', $contentId);
463
        }
464
465
        $versionInfo = $this->mapper->extractVersionInfoListFromRows(
466
            $rows,
467
            $this->contentGateway->loadVersionedNameData(array(array('id' => $contentId, 'version' => $versionNo)))
468
        );
469
470
        return reset($versionInfo);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($versionInfo); of type eZ\Publish\SPI\Persisten...ntent\VersionInfo|false adds false to the return on line 470 which is incompatible with the return type declared by the interface eZ\Publish\SPI\Persisten...andler::loadVersionInfo of type eZ\Publish\SPI\Persistence\Content\VersionInfo. It seems like you forgot to handle an error condition.
Loading history...
471
    }
472
473
    /**
474
     * Returns all versions with draft status created by the given $userId.
475
     *
476
     * @param int $userId
477
     *
478
     * @return \eZ\Publish\SPI\Persistence\Content\VersionInfo[]
479
     */
480 View Code Duplication
    public function loadDraftsForUser($userId)
481
    {
482
        $rows = $this->contentGateway->listVersionsForUser($userId, VersionInfo::STATUS_DRAFT);
483
        if (empty($rows)) {
484
            return array();
485
        }
486
487
        $idVersionPairs = array_map(
488
            function ($row) {
489
                return array(
490
                    'id' => $row['ezcontentobject_version_contentobject_id'],
491
                    'version' => $row['ezcontentobject_version_version'],
492
                );
493
            },
494
            $rows
495
        );
496
        $nameRows = $this->contentGateway->loadVersionedNameData($idVersionPairs);
497
498
        return $this->mapper->extractVersionInfoListFromRows($rows, $nameRows);
499
    }
500
501
    /**
502
     * Sets the status of object identified by $contentId and $version to $status.
503
     *
504
     * The $status can be one of VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED
505
     * When status is set to VersionInfo::STATUS_PUBLISHED content status is updated to ContentInfo::STATUS_PUBLISHED
506
     *
507
     * @param int $contentId
508
     * @param int $status
509
     * @param int $version
510
     *
511
     * @return bool
512
     */
513
    public function setStatus($contentId, $status, $version)
514
    {
515
        return $this->contentGateway->setStatus($contentId, $version, $status);
516
    }
517
518
    /**
519
     * Updates a content object meta data, identified by $contentId.
520
     *
521
     * @param int $contentId
522
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $content
523
     *
524
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo
525
     */
526
    public function updateMetadata($contentId, MetadataUpdateStruct $content)
527
    {
528
        $this->contentGateway->updateContent($contentId, $content);
529
        $this->updatePathIdentificationString($contentId, $content);
530
531
        return $this->loadContentInfo($contentId);
532
    }
533
534
    /**
535
     * Updates path identification string for locations of given $contentId if main language
536
     * is set in update struct.
537
     *
538
     * This is specific to the Legacy storage engine, as path identification string is deprecated.
539
     *
540
     * @param int $contentId
541
     * @param \eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct $content
542
     */
543
    protected function updatePathIdentificationString($contentId, MetadataUpdateStruct $content)
544
    {
545
        if (isset($content->mainLanguageId)) {
546
            $contentLocationsRows = $this->locationGateway->loadLocationDataByContent($contentId);
547
            foreach ($contentLocationsRows as $row) {
548
                $locationName = '';
549
                $urlAliasRows = $this->urlAliasGateway->loadLocationEntries(
550
                    $row['node_id'],
551
                    false,
552
                    $content->mainLanguageId
553
                );
554
                if (!empty($urlAliasRows)) {
555
                    $locationName = $urlAliasRows[0]['text'];
556
                }
557
                $this->locationGateway->updatePathIdentificationString(
558
                    $row['node_id'],
559
                    $row['parent_node_id'],
560
                    $this->slugConverter->convert(
561
                        $locationName,
562
                        'node_' . $row['node_id'],
563
                        'urlalias_compat'
564
                    )
565
                );
566
            }
567
        }
568
    }
569
570
    /**
571
     * Updates a content version, identified by $contentId and $versionNo.
572
     *
573
     * @param int $contentId
574
     * @param int $versionNo
575
     * @param \eZ\Publish\SPI\Persistence\Content\UpdateStruct $updateStruct
576
     *
577
     * @return \eZ\Publish\SPI\Persistence\Content
578
     */
579
    public function updateContent($contentId, $versionNo, UpdateStruct $updateStruct)
580
    {
581
        $content = $this->load($contentId, $versionNo);
582
        $this->contentGateway->updateVersion($contentId, $versionNo, $updateStruct);
583
        $contentType = $this->contentTypeHandler->load($content->versionInfo->contentInfo->contentTypeId);
584
        $this->fieldHandler->updateFields($content, $updateStruct, $contentType);
585
        foreach ($updateStruct->name as $language => $name) {
586
            $this->contentGateway->setName(
587
                $contentId,
588
                $versionNo,
589
                $name,
590
                $language
591
            );
592
        }
593
594
        return $this->load($contentId, $versionNo);
595
    }
596
597
    /**
598
     * Deletes all versions and fields, all locations (subtree), and all relations.
599
     *
600
     * Removes the relations, but not the related objects. All subtrees of the
601
     * assigned nodes of this content objects are removed (recursively).
602
     *
603
     * @param int $contentId
604
     *
605
     * @return bool
606
     */
607
    public function deleteContent($contentId)
608
    {
609
        $contentLocations = $this->contentGateway->getAllLocationIds($contentId);
610
        if (empty($contentLocations)) {
611
            $this->removeRawContent($contentId);
612
        } else {
613
            foreach ($contentLocations as $locationId) {
614
                $this->treeHandler->removeSubtree($locationId);
615
            }
616
        }
617
    }
618
619
    /**
620
     * Deletes raw content data.
621
     *
622
     * @param int $contentId
623
     */
624
    public function removeRawContent($contentId)
625
    {
626
        $this->treeHandler->removeRawContent($contentId);
627
    }
628
629
    /**
630
     * Deletes given version, its fields, node assignment, relations and names.
631
     *
632
     * Removes the relations, but not the related objects.
633
     *
634
     * @param int $contentId
635
     * @param int $versionNo
636
     *
637
     * @return bool
638
     */
639
    public function deleteVersion($contentId, $versionNo)
640
    {
641
        $versionInfo = $this->loadVersionInfo($contentId, $versionNo);
642
643
        $this->locationGateway->deleteNodeAssignment($contentId, $versionNo);
644
645
        $this->fieldHandler->deleteFields($contentId, $versionInfo);
646
647
        $this->contentGateway->deleteRelations($contentId, $versionNo);
648
        $this->contentGateway->deleteVersions($contentId, $versionNo);
649
        $this->contentGateway->deleteNames($contentId, $versionNo);
650
    }
651
652
    /**
653
     * Returns the versions for $contentId.
654
     *
655
     * Result is returned with oldest version first (sorted by created, alternatively version id if auto increment).
656
     *
657
     * @param int $contentId
658
     * @param mixed|null $status Optional argument to filter versions by status, like {@see VersionInfo::STATUS_ARCHIVED}.
659
     * @param int $limit Limit for items returned, -1 means none.
660
     *
661
     * @return \eZ\Publish\SPI\Persistence\Content\VersionInfo[]
662
     */
663
    public function listVersions($contentId, $status = null, $limit = -1)
664
    {
665
        return $this->treeHandler->listVersions($contentId, $status, $limit);
666
    }
667
668
    /**
669
     * Copy Content with Fields, Versions & Relations from $contentId in $version.
670
     *
671
     * {@inheritdoc}
672
     *
673
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If content or version is not found
674
     *
675
     * @param mixed $contentId
676
     * @param mixed|null $versionNo Copy all versions if left null
677
     * @param int|null $newOwnerId
678
     *
679
     * @return \eZ\Publish\SPI\Persistence\Content
680
     */
681
    public function copy($contentId, $versionNo = null, $newOwnerId = null)
682
    {
683
        $currentVersionNo = isset($versionNo) ?
684
            $versionNo :
685
            $this->loadContentInfo($contentId)->currentVersionNo;
686
687
        // Copy content in given version or current version
688
        $createStruct = $this->mapper->createCreateStructFromContent(
689
            $this->load($contentId, $currentVersionNo)
690
        );
691
        if ($newOwnerId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newOwnerId 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...
692
            $createStruct->ownerId = $newOwnerId;
693
        }
694
        $content = $this->internalCreate($createStruct, $currentVersionNo);
695
696
        // If version was not passed also copy other versions
697
        if (!isset($versionNo)) {
698
            $contentType = $this->contentTypeHandler->load($createStruct->typeId);
699
700
            foreach ($this->listVersions($contentId) as $versionInfo) {
701
                if ($versionInfo->versionNo === $currentVersionNo) {
702
                    continue;
703
                }
704
705
                $versionContent = $this->load($contentId, $versionInfo->versionNo);
706
707
                $versionContent->versionInfo->contentInfo->id = $content->versionInfo->contentInfo->id;
708
                $versionContent->versionInfo->modificationDate = $createStruct->modified;
709
                $versionContent->versionInfo->creationDate = $createStruct->modified;
710
                $versionContent->versionInfo->id = $this->contentGateway->insertVersion(
711
                    $versionContent->versionInfo,
712
                    $versionContent->fields
713
                );
714
715
                $this->fieldHandler->createNewFields($versionContent, $contentType);
716
717
                // Create names
718 View Code Duplication
                foreach ($versionContent->versionInfo->names as $language => $name) {
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...
719
                    $this->contentGateway->setName(
720
                        $content->versionInfo->contentInfo->id,
721
                        $versionInfo->versionNo,
722
                        $name,
723
                        $language
724
                    );
725
                }
726
            }
727
728
            // Batch copy relations for all versions
729
            $this->contentGateway->copyRelations($contentId, $content->versionInfo->contentInfo->id);
730
        } else {
731
            // Batch copy relations for published version
732
            $this->contentGateway->copyRelations($contentId, $content->versionInfo->contentInfo->id, $versionNo);
733
        }
734
735
        return $content;
736
    }
737
738
    /**
739
     * Creates a relation between $sourceContentId in $sourceContentVersionNo
740
     * and $destinationContentId with a specific $type.
741
     *
742
     * @todo Should the existence verifications happen here or is this supposed to be handled at a higher level?
743
     *
744
     * @param \eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct $createStruct
745
     *
746
     * @return \eZ\Publish\SPI\Persistence\Content\Relation
747
     */
748
    public function addRelation(RelationCreateStruct $createStruct)
749
    {
750
        $relation = $this->mapper->createRelationFromCreateStruct($createStruct);
751
752
        $relation->id = $this->contentGateway->insertRelation($createStruct);
753
754
        return $relation;
755
    }
756
757
    /**
758
     * Removes a relation by relation Id.
759
     *
760
     * @todo Should the existence verifications happen here or is this supposed to be handled at a higher level?
761
     *
762
     * @param mixed $relationId
763
     * @param int $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
764
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
765
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
766
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
767
     */
768
    public function removeRelation($relationId, $type)
769
    {
770
        $this->contentGateway->deleteRelation($relationId, $type);
771
    }
772
773
    /**
774
     * Loads relations from $sourceContentId. Optionally, loads only those with $type and $sourceContentVersionNo.
775
     *
776
     * @param mixed $sourceContentId Source Content ID
777
     * @param mixed|null $sourceContentVersionNo Source Content Version, null if not specified
778
     * @param int|null $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
779
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
780
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
781
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
782
     *
783
     * @return \eZ\Publish\SPI\Persistence\Content\Relation[]
784
     */
785
    public function loadRelations($sourceContentId, $sourceContentVersionNo = null, $type = null)
786
    {
787
        return $this->mapper->extractRelationsFromRows(
788
            $this->contentGateway->loadRelations($sourceContentId, $sourceContentVersionNo, $type)
789
        );
790
    }
791
792
    /**
793
     * Loads relations from $contentId. Optionally, loads only those with $type.
794
     *
795
     * Only loads relations against published versions.
796
     *
797
     * @param mixed $destinationContentId Destination Content ID
798
     * @param int|null $type {@see \eZ\Publish\API\Repository\Values\Content\Relation::COMMON,
799
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::EMBED,
800
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::LINK,
801
     *                 \eZ\Publish\API\Repository\Values\Content\Relation::FIELD}
802
     *
803
     * @return \eZ\Publish\SPI\Persistence\Content\Relation[]
804
     */
805
    public function loadReverseRelations($destinationContentId, $type = null)
806
    {
807
        return $this->mapper->extractRelationsFromRows(
808
            $this->contentGateway->loadReverseRelations($destinationContentId, $type)
809
        );
810
    }
811
812
    /**
813
     * {@inheritdoc}
814
     */
815
    public function removeTranslationFromContent($contentId, $languageCode)
816
    {
817
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
818
            __METHOD__ . ' is deprecated, use deleteTranslationFromContent instead',
819
            E_USER_DEPRECATED
820
        );
821
        $this->deleteTranslationFromContent($contentId, $languageCode);
822
    }
823
824
    /**
825
     * {@inheritdoc}
826
     */
827
    public function deleteTranslationFromContent($contentId, $languageCode)
828
    {
829
        $this->fieldHandler->deleteTranslationFromContentFields(
830
            $contentId,
831
            $this->listVersions($contentId),
832
            $languageCode
833
        );
834
        $this->contentGateway->deleteTranslationFromContent($contentId, $languageCode);
835
    }
836
837
    /**
838
     * {@inheritdoc}
839
     */
840
    public function deleteTranslationFromDraft($contentId, $versionNo, $languageCode)
841
    {
842
        $versionInfo = $this->loadVersionInfo($contentId, $versionNo);
843
844
        $this->fieldHandler->deleteTranslationFromVersionFields(
845
            $versionInfo,
846
            $languageCode
847
        );
848
        $this->contentGateway->deleteTranslationFromVersion(
849
            $contentId,
850
            $versionNo,
851
            $languageCode
852
        );
853
854
        // get all [languageCode => name] entries except the removed Translation
855
        $names = array_filter(
856
            $versionInfo->names,
857
            function ($lang) use ($languageCode) {
858
                return $lang !== $languageCode;
859
            },
860
            ARRAY_FILTER_USE_KEY
861
        );
862
        // set new Content name
863
        foreach ($names as $language => $name) {
864
            $this->contentGateway->setName(
865
                $contentId,
866
                $versionNo,
867
                $name,
868
                $language
869
            );
870
        }
871
872
        // reload entire Version w/o removed Translation
873
        return $this->load($contentId, $versionNo);
874
    }
875
}
876