Completed
Push — ezp25366-copy_content_relation... ( 7d5327...4b97d3 )
by André
74:42 queued 12:30
created

ContentService::internalPublishVersion()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 15
nc 3
nop 2
1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\ContentService 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
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\Repository;
12
13
use eZ\Publish\API\Repository\ContentService as ContentServiceInterface;
14
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
15
use eZ\Publish\SPI\Persistence\Handler;
16
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
17
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
18
use eZ\Publish\API\Repository\Values\Content\TranslationInfo;
19
use eZ\Publish\API\Repository\Values\Content\TranslationValues as APITranslationValues;
20
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
21
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
22
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
23
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
24
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
25
use eZ\Publish\API\Repository\Values\User\User;
26
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
27
use eZ\Publish\API\Repository\Values\Content\Field;
28
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
29
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
30
use eZ\Publish\Core\Base\Exceptions\BadStateException;
31
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
32
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
33
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
34
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
35
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
36
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
37
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
38
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
39
use eZ\Publish\Core\Repository\Values\Content\TranslationValues;
40
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
41
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
42
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
43
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
44
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
45
use Exception;
46
use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
47
48
/**
49
 * This class provides service methods for managing content.
50
 *
51
 * @example Examples/content.php
52
 */
53
class ContentService implements ContentServiceInterface
54
{
55
    /**
56
     * @var \eZ\Publish\Core\Repository\Repository
57
     */
58
    protected $repository;
59
60
    /**
61
     * @var \eZ\Publish\SPI\Persistence\Handler
62
     */
63
    protected $persistenceHandler;
64
65
    /**
66
     * @var array
67
     */
68
    protected $settings;
69
70
    /**
71
     * @var \eZ\Publish\Core\Repository\Helper\DomainMapper
72
     */
73
    protected $domainMapper;
74
75
    /**
76
     * @var \eZ\Publish\Core\Repository\Helper\RelationProcessor
77
     */
78
    protected $relationProcessor;
79
80
    /**
81
     * @var \eZ\Publish\Core\Repository\Helper\NameSchemaService
82
     */
83
    protected $nameSchemaService;
84
85
    /**
86
     * @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry
87
     */
88
    protected $fieldTypeRegistry;
89
90
    /**
91
     * Setups service with reference to repository object that created it & corresponding handler.
92
     *
93
     * @param \eZ\Publish\API\Repository\Repository $repository
94
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
95
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
96
     * @param \eZ\Publish\Core\Repository\Helper\RelationProcessor $relationProcessor
97
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
98
     * @param \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry $fieldTypeRegistry,
0 ignored issues
show
Documentation introduced by
There is no parameter named $fieldTypeRegistry,. Did you maybe mean $fieldTypeRegistry?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
99
     * @param array $settings
100
     */
101 View Code Duplication
    public function __construct(
102
        RepositoryInterface $repository,
103
        Handler $handler,
104
        Helper\DomainMapper $domainMapper,
105
        Helper\RelationProcessor $relationProcessor,
106
        Helper\NameSchemaService $nameSchemaService,
107
        Helper\FieldTypeRegistry $fieldTypeRegistry,
108
        array $settings = array()
109
    ) {
110
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
$repository is of type object<eZ\Publish\API\Repository\Repository>, but the property $repository was declared to be of type object<eZ\Publish\Core\Repository\Repository>. 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...
111
        $this->persistenceHandler = $handler;
112
        $this->domainMapper = $domainMapper;
113
        $this->relationProcessor = $relationProcessor;
114
        $this->nameSchemaService = $nameSchemaService;
115
        $this->fieldTypeRegistry = $fieldTypeRegistry;
116
        // Union makes sure default settings are ignored if provided in argument
117
        $this->settings = $settings + array(
118
            //'defaultSetting' => array(),
119
        );
120
    }
121
122
    /**
123
     * Loads a content info object.
124
     *
125
     * To load fields use loadContent
126
     *
127
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
128
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
129
     *
130
     * @param int $contentId
131
     *
132
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
133
     */
134 View Code Duplication
    public function loadContentInfo($contentId)
135
    {
136
        $contentInfo = $this->internalLoadContentInfo($contentId);
137
        if (!$this->repository->canUser('content', 'read', $contentInfo)) {
138
            throw new UnauthorizedException('content', 'read', array('contentId' => $contentId));
139
        }
140
141
        return $contentInfo;
142
    }
143
144
    /**
145
     * Loads a content info object.
146
     *
147
     * To load fields use loadContent
148
     *
149
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
150
     *
151
     * @param mixed $id
152
     * @param bool $isRemoteId
153
     *
154
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
155
     */
156
    public function internalLoadContentInfo($id, $isRemoteId = false)
157
    {
158
        try {
159
            $method = $isRemoteId ? 'loadContentInfoByRemoteId' : 'loadContentInfo';
160
161
            return $this->domainMapper->buildContentInfoDomainObject(
162
                $this->persistenceHandler->contentHandler()->$method($id)
163
            );
164
        } catch (APINotFoundException $e) {
165
            throw new NotFoundException(
166
                'Content',
167
                $id,
168
                $e
169
            );
170
        }
171
    }
172
173
    /**
174
     * Loads a content info object for the given remoteId.
175
     *
176
     * To load fields use loadContent
177
     *
178
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
179
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
180
     *
181
     * @param string $remoteId
182
     *
183
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
184
     */
185 View Code Duplication
    public function loadContentInfoByRemoteId($remoteId)
186
    {
187
        $contentInfo = $this->internalLoadContentInfo($remoteId, true);
188
189
        if (!$this->repository->canUser('content', 'read', $contentInfo)) {
190
            throw new UnauthorizedException('content', 'read', array('remoteId' => $remoteId));
191
        }
192
193
        return $contentInfo;
194
    }
195
196
    /**
197
     * Loads a version info of the given content object.
198
     *
199
     * If no version number is given, the method returns the current version
200
     *
201
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
202
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
203
     *
204
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
205
     * @param int $versionNo the version number. If not given the current version is returned.
206
     *
207
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
208
     */
209
    public function loadVersionInfo(ContentInfo $contentInfo, $versionNo = null)
210
    {
211
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
212
    }
213
214
    /**
215
     * Loads a version info of the given content object id.
216
     *
217
     * If no version number is given, the method returns the current version
218
     *
219
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
220
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
221
     *
222
     * @param mixed $contentId
223
     * @param int $versionNo the version number. If not given the current version is returned.
224
     *
225
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
226
     */
227
    public function loadVersionInfoById($contentId, $versionNo = null)
228
    {
229
        if ($versionNo === null) {
230
            $versionNo = $this->loadContentInfo($contentId)->currentVersionNo;
231
        }
232
233
        try {
234
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
235
                $contentId,
236
                $versionNo
237
            );
238
        } catch (APINotFoundException $e) {
239
            throw new NotFoundException(
240
                'VersionInfo',
241
                array(
242
                    'contentId' => $contentId,
243
                    'versionNo' => $versionNo,
244
                ),
245
                $e
246
            );
247
        }
248
249
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
250
251
        if ($versionInfo->status === APIVersionInfo::STATUS_PUBLISHED) {
252
            $function = 'read';
253
        } else {
254
            $function = 'versionread';
255
        }
256
257
        if (!$this->repository->canUser('content', $function, $versionInfo)) {
258
            throw new UnauthorizedException('content', $function, array('contentId' => $contentId));
259
        }
260
261
        return $versionInfo;
262
    }
263
264
    /**
265
     * Loads content in a version for the given content info object.
266
     *
267
     * If no version number is given, the method returns the current version
268
     *
269
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if version with the given number does not exist
270
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
271
     *
272
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
273
     * @param array $languages A language filter for fields. If not given all languages are returned
274
     * @param int $versionNo the version number. If not given the current version is returned
275
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
276
     *
277
     * @return \eZ\Publish\API\Repository\Values\Content\Content
278
     */
279
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
280
    {
281
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
282
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
283
            $useAlwaysAvailable = false;
284
        }
285
286
        return $this->loadContent(
287
            $contentInfo->id,
288
            $languages,
289
            $versionNo,
290
            $useAlwaysAvailable
291
        );
292
    }
293
294
    /**
295
     * Loads content in the version given by version info.
296
     *
297
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
298
     *
299
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
300
     * @param array $languages A language filter for fields. If not given all languages are returned
301
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
302
     *
303
     * @return \eZ\Publish\API\Repository\Values\Content\Content
304
     */
305
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, $useAlwaysAvailable = true)
306
    {
307
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
308
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
309
            $useAlwaysAvailable = false;
310
        }
311
312
        return $this->loadContent(
313
            $versionInfo->getContentInfo()->id,
314
            $languages,
315
            $versionInfo->versionNo,
316
            $useAlwaysAvailable
317
        );
318
    }
319
320
    /**
321
     * Loads content in a version of the given content object.
322
     *
323
     * If no version number is given, the method returns the current version
324
     *
325
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
326
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions
327
     *
328
     * @param int $contentId
329
     * @param array|null $languages A language filter for fields. If not given all languages are returned
330
     * @param int|null $versionNo the version number. If not given the current version is returned
331
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
332
     *
333
     * @return \eZ\Publish\API\Repository\Values\Content\Content
334
     */
335 View Code Duplication
    public function loadContent($contentId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
336
    {
337
        $content = $this->internalLoadContent($contentId, $languages, $versionNo, false, $useAlwaysAvailable);
338
339
        if (!$this->repository->canUser('content', 'read', $content)) {
340
            throw new UnauthorizedException('content', 'read', array('contentId' => $contentId));
341
        }
342
343
        if (
344
            $content->getVersionInfo()->status !== APIVersionInfo::STATUS_PUBLISHED
345
            && !$this->repository->canUser('content', 'versionread', $content)
346
        ) {
347
            throw new UnauthorizedException('content', 'versionread', array('contentId' => $contentId, 'versionNo' => $versionNo));
348
        }
349
350
        return $content;
351
    }
352
353
    /**
354
     * Loads content in a version of the given content object.
355
     *
356
     * If no version number is given, the method returns the current version
357
     *
358
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
359
     *
360
     * @param mixed $id
361
     * @param array|null $languages A language filter for fields. If not given all languages are returned
362
     * @param int|null $versionNo the version number. If not given the current version is returned
363
     * @param bool $isRemoteId
364
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
365
     *
366
     * @return \eZ\Publish\API\Repository\Values\Content\Content
367
     */
368
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
369
    {
370
        try {
371
            // Get Content ID if lookup by remote ID
372
            if ($isRemoteId) {
373
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
374
                $id = $spiContentInfo->id;
375
            }
376
377
            // Get current version if $versionNo is not defined
378
            if ($versionNo === null) {
379
                if (!isset($spiContentInfo)) {
380
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
381
                }
382
383
                $versionNo = $spiContentInfo->currentVersionNo;
384
            }
385
386
            $loadLanguages = $languages;
387
            $alwaysAvailableLanguageCode = null;
388
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
389
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
390
                if (!isset($spiContentInfo)) {
391
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
392
                }
393
394
                if ($spiContentInfo->alwaysAvailable) {
395
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
396
                    $loadLanguages = array_unique($loadLanguages);
397
                }
398
            }
399
400
            $spiContent = $this->persistenceHandler->contentHandler()->load(
401
                $id,
402
                $versionNo,
403
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 386 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
404
            );
405
        } catch (APINotFoundException $e) {
406
            throw new NotFoundException(
407
                'Content',
408
                array(
409
                    $isRemoteId ? 'remoteId' : 'id' => $id,
410
                    'languages' => $languages,
411
                    'versionNo' => $versionNo,
412
                ),
413
                $e
414
            );
415
        }
416
417
        return $this->domainMapper->buildContentDomainObject(
418
            $spiContent,
419
            null,
420
            empty($languages) ? null : $languages,
421
            $alwaysAvailableLanguageCode
422
        );
423
    }
424
425
    /**
426
     * Loads content in a version for the content object reference by the given remote id.
427
     *
428
     * If no version is given, the method returns the current version
429
     *
430
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
431
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions
432
     *
433
     * @param string $remoteId
434
     * @param array $languages A language filter for fields. If not given all languages are returned
435
     * @param int $versionNo the version number. If not given the current version is returned
436
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
437
     *
438
     * @return \eZ\Publish\API\Repository\Values\Content\Content
439
     */
440 View Code Duplication
    public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
441
    {
442
        $content = $this->internalLoadContent($remoteId, $languages, $versionNo, true, $useAlwaysAvailable);
443
444
        if (!$this->repository->canUser('content', 'read', $content)) {
445
            throw new UnauthorizedException('content', 'read', array('remoteId' => $remoteId));
446
        }
447
448
        if (
449
            $content->getVersionInfo()->status !== APIVersionInfo::STATUS_PUBLISHED
450
            && !$this->repository->canUser('content', 'versionread', $content)
451
        ) {
452
            throw new UnauthorizedException('content', 'versionread', array('remoteId' => $remoteId, 'versionNo' => $versionNo));
453
        }
454
455
        return $content;
456
    }
457
458
    /**
459
     * Creates a new content draft assigned to the authenticated user.
460
     *
461
     * If a different userId is given in $contentCreateStruct it is assigned to the given user
462
     * but this required special rights for the authenticated user
463
     * (this is useful for content staging where the transfer process does not
464
     * have to authenticate with the user which created the content object in the source server).
465
     * The user has to publish the draft if it should be visible.
466
     * In 4.x at least one location has to be provided in the location creation array.
467
     *
468
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
469
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is a provided remoteId which exists in the system
470
     *                                                                        or there is no location provided (4.x) or multiple locations
471
     *                                                                        are under the same parent or if the a field value is not accepted by the field type
472
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid
473
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or is set to an empty value
474
     *
475
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
476
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
477
     *
478
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
479
     */
480
    public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = array())
481
    {
482
        if ($contentCreateStruct->mainLanguageCode === null) {
483
            throw new InvalidArgumentException('$contentCreateStruct', "'mainLanguageCode' property must be set");
484
        }
485
486
        if ($contentCreateStruct->contentType === null) {
487
            throw new InvalidArgumentException('$contentCreateStruct', "'contentType' property must be set");
488
        }
489
490
        $contentCreateStruct = clone $contentCreateStruct;
491
492
        if ($contentCreateStruct->ownerId === null) {
493
            $contentCreateStruct->ownerId = $this->repository->getCurrentUserReference()->getUserId();
494
        }
495
496
        if ($contentCreateStruct->alwaysAvailable === null) {
497
            $contentCreateStruct->alwaysAvailable = false;
498
        }
499
500
        $contentCreateStruct->contentType = $this->repository->getContentTypeService()->loadContentType(
501
            $contentCreateStruct->contentType->id
502
        );
503
504
        if (empty($contentCreateStruct->sectionId)) {
505
            if (isset($locationCreateStructs[0])) {
506
                $location = $this->repository->getLocationService()->loadLocation(
507
                    $locationCreateStructs[0]->parentLocationId
508
                );
509
                $contentCreateStruct->sectionId = $location->contentInfo->sectionId;
510
            } else {
511
                $contentCreateStruct->sectionId = 1;
512
            }
513
        }
514
515
        if (!$this->repository->canUser('content', 'create', $contentCreateStruct, $locationCreateStructs)) {
516
            throw new UnauthorizedException(
517
                'content',
518
                'create',
519
                array(
520
                    'parentLocationId' => isset($locationCreateStructs[0]) ?
521
                            $locationCreateStructs[0]->parentLocationId :
522
                            null,
523
                    'sectionId' => $contentCreateStruct->sectionId,
524
                )
525
            );
526
        }
527
528
        if (!empty($contentCreateStruct->remoteId)) {
529
            try {
530
                $this->loadContentByRemoteId($contentCreateStruct->remoteId);
531
532
                throw new InvalidArgumentException(
533
                    '$contentCreateStruct',
534
                    "Another content with remoteId '{$contentCreateStruct->remoteId}' exists"
535
                );
536
            } catch (APINotFoundException $e) {
537
                // Do nothing
538
            }
539
        } else {
540
            $contentCreateStruct->remoteId = $this->domainMapper->getUniqueHash($contentCreateStruct);
541
        }
542
543
        $spiLocationCreateStructs = $this->buildSPILocationCreateStructs($locationCreateStructs);
0 ignored issues
show
Documentation introduced by
$locationCreateStructs is of type array<integer,object<eZ\...tionCreateStruct>|null>, but the function expects a array<integer,object<eZ\...\LocationCreateStruct>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
544
545
        $languageCodes = $this->getLanguageCodesForCreate($contentCreateStruct);
546
        $fields = $this->mapFieldsForCreate($contentCreateStruct);
547
548
        $fieldValues = array();
549
        $spiFields = array();
550
        $allFieldErrors = array();
551
        $inputRelations = array();
552
        $locationIdToContentIdMapping = array();
553
554
        foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
555
            /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
556
            $fieldType = $this->fieldTypeRegistry->getFieldType(
557
                $fieldDefinition->fieldTypeIdentifier
558
            );
559
560
            foreach ($languageCodes as $languageCode) {
561
                $isEmptyValue = false;
562
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $contentCreateStruct->mainLanguageCode;
563
                $isLanguageMain = $languageCode === $contentCreateStruct->mainLanguageCode;
564
                if (isset($fields[$fieldDefinition->identifier][$valueLanguageCode])) {
565
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
566
                } else {
567
                    $fieldValue = $fieldDefinition->defaultValue;
0 ignored issues
show
Documentation introduced by
The property $defaultValue is declared protected in eZ\Publish\API\Repositor...entType\FieldDefinition. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
568
                }
569
570
                $fieldValue = $fieldType->acceptValue($fieldValue);
571
572 View Code Duplication
                if ($fieldType->isEmptyValue($fieldValue)) {
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...
573
                    $isEmptyValue = true;
574
                    if ($fieldDefinition->isRequired) {
575
                        throw new ContentValidationException(
576
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
577
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode]
578
                        );
579
                    }
580
                } else {
581
                    $fieldErrors = $fieldType->validate(
582
                        $fieldDefinition,
583
                        $fieldValue
584
                    );
585
                    if (!empty($fieldErrors)) {
586
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
587
                    }
588
                }
589
590
                if (!empty($allFieldErrors)) {
591
                    continue;
592
                }
593
594
                $this->relationProcessor->appendFieldRelations(
595
                    $inputRelations,
596
                    $locationIdToContentIdMapping,
597
                    $fieldType,
598
                    $fieldValue,
599
                    $fieldDefinition->id
600
                );
601
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
602
603
                // Only non-empty value for: translatable field or in main language
604
                if (
605
                    (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
606
                    (!$isEmptyValue && $isLanguageMain)
607
                ) {
608
                    $spiFields[] = new SPIField(
609
                        array(
610
                            'id' => null,
611
                            'fieldDefinitionId' => $fieldDefinition->id,
612
                            'type' => $fieldDefinition->fieldTypeIdentifier,
613
                            'value' => $fieldType->toPersistenceValue($fieldValue),
614
                            'languageCode' => $languageCode,
615
                            'versionNo' => null,
616
                        )
617
                    );
618
                }
619
            }
620
        }
621
622
        if (!empty($allFieldErrors)) {
623
            throw new ContentFieldValidationException($allFieldErrors);
624
        }
625
626
        $spiContentCreateStruct = new SPIContentCreateStruct(
627
            array(
628
                'name' => $this->nameSchemaService->resolve(
629
                    $contentCreateStruct->contentType->nameSchema,
630
                    $contentCreateStruct->contentType,
631
                    $fieldValues,
632
                    $languageCodes
633
                ),
634
                'typeId' => $contentCreateStruct->contentType->id,
635
                'sectionId' => $contentCreateStruct->sectionId,
636
                'ownerId' => $contentCreateStruct->ownerId,
637
                'locations' => $spiLocationCreateStructs,
638
                'fields' => $spiFields,
639
                'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
640
                'remoteId' => $contentCreateStruct->remoteId,
641
                'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
642
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
643
                    $contentCreateStruct->mainLanguageCode
644
                )->id,
645
            )
646
        );
647
648
        $defaultObjectStates = $this->getDefaultObjectStates();
649
650
        $this->repository->beginTransaction();
651
        try {
652
            $spiContent = $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
653
            $this->relationProcessor->processFieldRelations(
654
                $inputRelations,
655
                $spiContent->versionInfo->contentInfo->id,
656
                $spiContent->versionInfo->versionNo,
657
                $contentCreateStruct->contentType
658
            );
659
660
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
661
                $this->persistenceHandler->objectStateHandler()->setContentState(
662
                    $spiContent->versionInfo->contentInfo->id,
663
                    $objectStateGroupId,
664
                    $objectState->id
665
                );
666
            }
667
668
            $this->repository->commit();
669
        } catch (Exception $e) {
670
            $this->repository->rollback();
671
            throw $e;
672
        }
673
674
        return $this->domainMapper->buildContentDomainObject($spiContent);
675
    }
676
677
    /**
678
     * Returns an array of default content states with content state group id as key.
679
     *
680
     * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
681
     */
682
    protected function getDefaultObjectStates()
683
    {
684
        $defaultObjectStatesMap = array();
685
        $objectStateHandler = $this->persistenceHandler->objectStateHandler();
686
687
        foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
688
            foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
689
                // Only register the first object state which is the default one.
690
                $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
691
                break;
692
            }
693
        }
694
695
        return $defaultObjectStatesMap;
696
    }
697
698
    /**
699
     * Returns all language codes used in given $fields.
700
     *
701
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value is set in main language
702
     *
703
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
704
     *
705
     * @return string[]
706
     */
707
    protected function getLanguageCodesForCreate(APIContentCreateStruct $contentCreateStruct)
708
    {
709
        $languageCodes = array();
710
711 View Code Duplication
        foreach ($contentCreateStruct->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...
712
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
713
                continue;
714
            }
715
716
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
717
                $field->languageCode
718
            );
719
            $languageCodes[$field->languageCode] = true;
720
        }
721
722
        if (!isset($languageCodes[$contentCreateStruct->mainLanguageCode])) {
723
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
724
                $contentCreateStruct->mainLanguageCode
725
            );
726
            $languageCodes[$contentCreateStruct->mainLanguageCode] = true;
727
        }
728
729
        return array_keys($languageCodes);
730
    }
731
732
    /**
733
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
734
     *
735
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
736
     *                                                                          or value is set for non-translatable field in language
737
     *                                                                          other than main
738
     *
739
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
740
     *
741
     * @return array
742
     */
743
    protected function mapFieldsForCreate(APIContentCreateStruct $contentCreateStruct)
744
    {
745
        $fields = array();
746
747
        foreach ($contentCreateStruct->fields as $field) {
748
            $fieldDefinition = $contentCreateStruct->contentType->getFieldDefinition($field->fieldDefIdentifier);
749
750
            if ($fieldDefinition === null) {
751
                throw new ContentValidationException(
752
                    "Field definition '%identifier%' does not exist in given ContentType",
753
                    ['%identifier%' => $field->fieldDefIdentifier]
754
                );
755
            }
756
757
            if ($field->languageCode === null) {
758
                $field = $this->cloneField(
759
                    $field,
760
                    array('languageCode' => $contentCreateStruct->mainLanguageCode)
761
                );
762
            }
763
764 View Code Duplication
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $contentCreateStruct->mainLanguageCode)) {
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...
765
                throw new ContentValidationException(
766
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
767
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
768
                );
769
            }
770
771
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
772
        }
773
774
        return $fields;
775
    }
776
777
    /**
778
     * Clones $field with overriding specific properties from given $overrides array.
779
     *
780
     * @param Field $field
781
     * @param array $overrides
782
     *
783
     * @return Field
784
     */
785
    private function cloneField(Field $field, array $overrides = array())
786
    {
787
        $fieldData = array_merge(
788
            array(
789
                'id' => $field->id,
790
                'value' => $field->value,
0 ignored issues
show
Documentation introduced by
The property $value is declared protected in eZ\Publish\API\Repository\Values\Content\Field. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
791
                'languageCode' => $field->languageCode,
792
                'fieldDefIdentifier' => $field->fieldDefIdentifier,
793
            ),
794
            $overrides
795
        );
796
797
        return new Field($fieldData);
798
    }
799
800
    /**
801
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
802
     *
803
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
804
     *
805
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
806
     */
807
    protected function buildSPILocationCreateStructs(array $locationCreateStructs)
808
    {
809
        $spiLocationCreateStructs = array();
810
        $parentLocationIdSet = array();
811
        $mainLocation = true;
812
813
        foreach ($locationCreateStructs as $locationCreateStruct) {
814
            if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
815
                throw new InvalidArgumentException(
816
                    '$locationCreateStructs',
817
                    "Multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}' are given"
818
                );
819
            }
820
821
            $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
822
            $parentLocation = $this->repository->getLocationService()->loadLocation(
823
                $locationCreateStruct->parentLocationId
824
            );
825
826
            $spiLocationCreateStructs[] = $this->domainMapper->buildSPILocationCreateStruct(
827
                $locationCreateStruct,
828
                $parentLocation,
829
                $mainLocation,
830
                // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
831
                null,
832
                null
833
            );
834
835
            // First Location in the list will be created as main Location
836
            $mainLocation = false;
837
        }
838
839
        return $spiLocationCreateStructs;
840
    }
841
842
    /**
843
     * Updates the metadata.
844
     *
845
     * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
846
     *
847
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
848
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
849
     *
850
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
851
     * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
852
     *
853
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
854
     */
855
    public function updateContentMetadata(ContentInfo $contentInfo, ContentMetadataUpdateStruct $contentMetadataUpdateStruct)
856
    {
857
        $propertyCount = 0;
858
        foreach ($contentMetadataUpdateStruct as $propertyName => $propertyValue) {
0 ignored issues
show
Bug introduced by
The expression $contentMetadataUpdateStruct of type object<eZ\Publish\API\Re...ntMetadataUpdateStruct> is not traversable.
Loading history...
859
            if (isset($contentMetadataUpdateStruct->$propertyName)) {
860
                $propertyCount += 1;
861
            }
862
        }
863
        if ($propertyCount === 0) {
864
            throw new InvalidArgumentException(
865
                '$contentMetadataUpdateStruct',
866
                'At least one property must be set'
867
            );
868
        }
869
870
        $loadedContentInfo = $this->loadContentInfo($contentInfo->id);
871
872
        if (!$this->repository->canUser('content', 'edit', $loadedContentInfo)) {
873
            throw new UnauthorizedException('content', 'edit', array('contentId' => $loadedContentInfo->id));
874
        }
875
876
        if (isset($contentMetadataUpdateStruct->remoteId)) {
877
            try {
878
                $existingContentInfo = $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
879
880
                if ($existingContentInfo->id !== $loadedContentInfo->id) {
881
                    throw new InvalidArgumentException(
882
                        '$contentMetadataUpdateStruct',
883
                        "Another content with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
884
                    );
885
                }
886
            } catch (APINotFoundException $e) {
887
                // Do nothing
888
            }
889
        }
890
891
        $this->repository->beginTransaction();
892
        try {
893
            if ($propertyCount > 1 || !isset($contentMetadataUpdateStruct->mainLocationId)) {
894
                $this->persistenceHandler->contentHandler()->updateMetadata(
895
                    $loadedContentInfo->id,
896
                    new SPIMetadataUpdateStruct(
897
                        array(
898
                            'ownerId' => $contentMetadataUpdateStruct->ownerId,
899
                            'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
900
                                $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
901
                                null,
902
                            'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
903
                                $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
904
                                null,
905
                            'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
906
                                $this->repository->getContentLanguageService()->loadLanguage(
907
                                    $contentMetadataUpdateStruct->mainLanguageCode
908
                                )->id :
909
                                null,
910
                            'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
911
                            'remoteId' => $contentMetadataUpdateStruct->remoteId,
912
                        )
913
                    )
914
                );
915
            }
916
917
            // Change main location
918
            if (isset($contentMetadataUpdateStruct->mainLocationId)
919
                && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
920
                $this->persistenceHandler->locationHandler()->changeMainLocation(
921
                    $loadedContentInfo->id,
922
                    $contentMetadataUpdateStruct->mainLocationId
923
                );
924
            }
925
926
            // Republish URL aliases to update always-available flag
927
            if (isset($contentMetadataUpdateStruct->alwaysAvailable)
928
                && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
929
                $content = $this->loadContent($loadedContentInfo->id);
930
                $this->publishUrlAliasesForContent($content, false);
931
            }
932
933
            $this->repository->commit();
934
        } catch (Exception $e) {
935
            $this->repository->rollback();
936
            throw $e;
937
        }
938
939
        return isset($content) ? $content : $this->loadContent($loadedContentInfo->id);
940
    }
941
942
    /**
943
     * Publishes URL aliases for all locations of a given content.
944
     *
945
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
946
     * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
947
     *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
948
     */
949
    protected function publishUrlAliasesForContent(APIContent $content, $updatePathIdentificationString = true)
950
    {
951
        $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
952
        $locations = $this->repository->getLocationService()->loadLocations(
953
            $content->getVersionInfo()->getContentInfo()
954
        );
955
        foreach ($locations as $location) {
956
            foreach ($urlAliasNames as $languageCode => $name) {
957
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
958
                    $location->id,
959
                    $location->parentLocationId,
960
                    $name,
961
                    $languageCode,
962
                    $content->contentInfo->alwaysAvailable,
963
                    $updatePathIdentificationString ? $languageCode === $content->contentInfo->mainLanguageCode : false
964
                );
965
            }
966
        }
967
    }
968
969
    /**
970
     * Deletes a content object including all its versions and locations including their subtrees.
971
     *
972
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to delete the content (in one of the locations of the given content object)
973
     *
974
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
975
     */
976
    public function deleteContent(ContentInfo $contentInfo)
977
    {
978
        $contentInfo = $this->internalLoadContentInfo($contentInfo->id);
979
980
        if (!$this->repository->canUser('content', 'remove', $contentInfo)) {
981
            throw new UnauthorizedException('content', 'remove', array('contentId' => $contentInfo->id));
982
        }
983
984
        $this->repository->beginTransaction();
985
        try {
986
            // Load Locations first as deleting Content also deletes belonging Locations
987
            $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
988
            $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
989
            foreach ($spiLocations as $spiLocation) {
990
                $this->persistenceHandler->urlAliasHandler()->locationDeleted($spiLocation->id);
991
            }
992
            $this->repository->commit();
993
        } catch (Exception $e) {
994
            $this->repository->rollback();
995
            throw $e;
996
        }
997
    }
998
999
    /**
1000
     * Creates a draft from a published or archived version.
1001
     *
1002
     * If no version is given, the current published version is used.
1003
     * 4.x: The draft is created with the initialLanguage code of the source version or if not present with the main language.
1004
     * It can be changed on updating the version.
1005
     *
1006
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
1007
     *
1008
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1009
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1010
     * @param \eZ\Publish\API\Repository\Values\User\User $creator if set given user is used to create the draft - otherwise the current-user is used
1011
     *
1012
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
1013
     */
1014
    public function createContentDraft(ContentInfo $contentInfo, APIVersionInfo $versionInfo = null, User $creator = null)
1015
    {
1016
        $contentInfo = $this->loadContentInfo($contentInfo->id);
1017
1018
        if ($versionInfo !== null) {
1019
            // Check that given $contentInfo and $versionInfo belong to the same content
1020
            if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
1021
                throw new InvalidArgumentException(
1022
                    '$versionInfo',
1023
                    'VersionInfo does not belong to the same content as given ContentInfo'
1024
                );
1025
            }
1026
1027
            $versionInfo = $this->loadVersionInfoById($contentInfo->id, $versionInfo->versionNo);
1028
1029
            switch ($versionInfo->status) {
1030
                case VersionInfo::STATUS_PUBLISHED:
1031
                case VersionInfo::STATUS_ARCHIVED:
1032
                    break;
1033
1034
                default:
1035
                    // @todo: throw an exception here, to be defined
1036
                    throw new BadStateException(
1037
                        '$versionInfo',
1038
                        'Draft can not be created from a draft version'
1039
                    );
1040
            }
1041
1042
            $versionNo = $versionInfo->versionNo;
1043
        } elseif ($contentInfo->published) {
1044
            $versionNo = $contentInfo->currentVersionNo;
1045
        } else {
1046
            // @todo: throw an exception here, to be defined
1047
            throw new BadStateException(
1048
                '$contentInfo',
1049
                'Content is not published, draft can be created only from published or archived version'
1050
            );
1051
        }
1052
1053
        if ($creator === null) {
1054
            $creator = $this->repository->getCurrentUserReference();
1055
        }
1056
1057
        if (!$this->repository->canUser('content', 'edit', $contentInfo)) {
1058
            throw new UnauthorizedException('content', 'edit', array('contentId' => $contentInfo->id));
1059
        }
1060
1061
        $this->repository->beginTransaction();
1062
        try {
1063
            $spiContent = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
1064
                $contentInfo->id,
1065
                $versionNo,
1066
                $creator->getUserId()
1067
            );
1068
            $this->repository->commit();
1069
        } catch (Exception $e) {
1070
            $this->repository->rollback();
1071
            throw $e;
1072
        }
1073
1074
        return $this->domainMapper->buildContentDomainObject($spiContent);
1075
    }
1076
1077
    /**
1078
     * Loads drafts for a user.
1079
     *
1080
     * If no user is given the drafts for the authenticated user a returned
1081
     *
1082
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to load the draft list
1083
     *
1084
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $user
1085
     *
1086
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo the drafts ({@link VersionInfo}) owned by the given user
1087
     */
1088
    public function loadContentDrafts(User $user = null)
1089
    {
1090
        if ($user === null) {
1091
            $user = $this->repository->getCurrentUserReference();
1092
        }
1093
1094
        // throw early if user has absolutely no access to versionread
1095
        if ($this->repository->hasAccess('content', 'versionread') === false) {
1096
            throw new UnauthorizedException('content', 'versionread');
1097
        }
1098
1099
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftsForUser($user->getUserId());
1100
        $versionInfoList = array();
1101 View Code Duplication
        foreach ($spiVersionInfoList as $spiVersionInfo) {
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...
1102
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1103
            if (!$this->repository->canUser('content', 'versionread', $versionInfo)) {
1104
                throw new UnauthorizedException('content', 'versionread', array('contentId' => $versionInfo->contentInfo->id));
1105
            }
1106
1107
            $versionInfoList[] = $versionInfo;
1108
        }
1109
1110
        return $versionInfoList;
1111
    }
1112
1113
    /**
1114
     * Translate a version.
1115
     *
1116
     * updates the destination version given in $translationInfo with the provided translated fields in $translationValues
1117
     *
1118
     * @example Examples/translation_5x.php
1119
     *
1120
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to update this version
1121
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the given destination version is not a draft
1122
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set to an empty value
1123
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $translationValues is not valid
1124
     *
1125
     * @param \eZ\Publish\API\Repository\Values\Content\TranslationInfo $translationInfo
1126
     * @param \eZ\Publish\API\Repository\Values\Content\TranslationValues $translationValues
1127
     * @param \eZ\Publish\API\Repository\Values\User\User $modifier If set, this user is taken as modifier of the version
1128
     *
1129
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the translated fields
1130
     *
1131
     * @since 5.0
1132
     */
1133
    public function translateVersion(TranslationInfo $translationInfo, APITranslationValues $translationValues, User $modifier = null)
1134
    {
1135
        throw new NotImplementedException(__METHOD__);
1136
    }
1137
1138
    /**
1139
     * Updates the fields of a draft.
1140
     *
1141
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1142
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1143
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentUpdateStruct is not valid
1144
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set to an empty value
1145
     *
1146
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1147
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1148
     *
1149
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1150
     */
1151
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
1152
    {
1153
        $contentUpdateStruct = clone $contentUpdateStruct;
1154
1155
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1156
        $content = $this->loadContent(
1157
            $versionInfo->getContentInfo()->id,
1158
            null,
1159
            $versionInfo->versionNo
1160
        );
1161
        if ($content->versionInfo->status !== APIVersionInfo::STATUS_DRAFT) {
1162
            throw new BadStateException(
1163
                '$versionInfo',
1164
                'Version is not a draft and can not be updated'
1165
            );
1166
        }
1167
1168
        if (!$this->repository->canUser('content', 'edit', $content)) {
1169
            throw new UnauthorizedException('content', 'edit', array('contentId' => $content->id));
1170
        }
1171
1172
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1173
        $languageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1174
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1175
            $content->contentInfo->contentTypeId
1176
        );
1177
        $fields = $this->mapFieldsForUpdate(
1178
            $contentUpdateStruct,
1179
            $contentType,
1180
            $mainLanguageCode
1181
        );
1182
1183
        $fieldValues = array();
1184
        $spiFields = array();
1185
        $allFieldErrors = array();
1186
        $inputRelations = array();
1187
        $locationIdToContentIdMapping = array();
1188
1189
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1190
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1191
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1192
                $fieldDefinition->fieldTypeIdentifier
1193
            );
1194
1195
            foreach ($languageCodes as $languageCode) {
1196
                $isCopied = $isEmpty = $isRetained = false;
1197
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1198
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1199
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1200
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1201
1202
                if (!$isFieldUpdated && !$isLanguageNew) {
1203
                    $isRetained = true;
1204
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
0 ignored issues
show
Documentation introduced by
The property $value is declared protected in eZ\Publish\API\Repository\Values\Content\Field. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1205
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1206
                    $isCopied = true;
1207
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
0 ignored issues
show
Documentation introduced by
The property $value is declared protected in eZ\Publish\API\Repository\Values\Content\Field. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1208
                } elseif ($isFieldUpdated) {
1209
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1210
                } else {
1211
                    $fieldValue = $fieldDefinition->defaultValue;
0 ignored issues
show
Documentation introduced by
The property $defaultValue is declared protected in eZ\Publish\API\Repositor...entType\FieldDefinition. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1212
                }
1213
1214
                $fieldValue = $fieldType->acceptValue($fieldValue);
1215
1216 View Code Duplication
                if ($fieldType->isEmptyValue($fieldValue)) {
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...
1217
                    $isEmpty = true;
1218
                    if ($fieldDefinition->isRequired) {
1219
                        throw new ContentValidationException(
1220
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1221
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode]
1222
                        );
1223
                    }
1224
                } else {
1225
                    $fieldErrors = $fieldType->validate(
1226
                        $fieldDefinition,
1227
                        $fieldValue
1228
                    );
1229
                    if (!empty($fieldErrors)) {
1230
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1231
                    }
1232
                }
1233
1234
                if (!empty($allFieldErrors)) {
1235
                    continue;
1236
                }
1237
1238
                $this->relationProcessor->appendFieldRelations(
1239
                    $inputRelations,
1240
                    $locationIdToContentIdMapping,
1241
                    $fieldType,
1242
                    $fieldValue,
0 ignored issues
show
Compatibility introduced by
$fieldValue of type object<eZ\Publish\SPI\FieldType\Value> is not a sub-type of object<eZ\Publish\Core\FieldType\Value>. It seems like you assume a concrete implementation of the interface eZ\Publish\SPI\FieldType\Value to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1243
                    $fieldDefinition->id
1244
                );
1245
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1246
1247
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1248
                    continue;
1249
                }
1250
1251
                $spiFields[] = new SPIField(
1252
                    array(
1253
                        'id' => $isLanguageNew ?
1254
                            null :
1255
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1256
                        'fieldDefinitionId' => $fieldDefinition->id,
1257
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1258
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1259
                        'languageCode' => $languageCode,
1260
                        'versionNo' => $versionInfo->versionNo,
1261
                    )
1262
                );
1263
            }
1264
        }
1265
1266
        if (!empty($allFieldErrors)) {
1267
            throw new ContentFieldValidationException($allFieldErrors);
1268
        }
1269
1270
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1271
            array(
1272
                'name' => $this->nameSchemaService->resolveNameSchema(
1273
                    $content,
1274
                    $fieldValues,
1275
                    $languageCodes,
1276
                    $contentType
1277
                ),
1278
                'creatorId' => $contentUpdateStruct->creatorId ?: $this->repository->getCurrentUserReference()->getUserId(),
1279
                'fields' => $spiFields,
1280
                'modificationDate' => time(),
1281
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1282
                    $contentUpdateStruct->initialLanguageCode
1283
                )->id,
1284
            )
1285
        );
1286
        $existingRelations = $this->loadRelations($versionInfo);
1287
1288
        $this->repository->beginTransaction();
1289
        try {
1290
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1291
                $versionInfo->getContentInfo()->id,
1292
                $versionInfo->versionNo,
1293
                $spiContentUpdateStruct
1294
            );
1295
            $this->relationProcessor->processFieldRelations(
1296
                $inputRelations,
1297
                $spiContent->versionInfo->contentInfo->id,
1298
                $spiContent->versionInfo->versionNo,
1299
                $contentType,
1300
                $existingRelations
1301
            );
1302
            $this->repository->commit();
1303
        } catch (Exception $e) {
1304
            $this->repository->rollback();
1305
            throw $e;
1306
        }
1307
1308
        return $this->domainMapper->buildContentDomainObject(
1309
            $spiContent,
1310
            $contentType
1311
        );
1312
    }
1313
1314
    /**
1315
     * Returns all language codes used in given $fields.
1316
     *
1317
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1318
     *
1319
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1320
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1321
     *
1322
     * @return array
1323
     */
1324
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content)
1325
    {
1326
        if ($contentUpdateStruct->initialLanguageCode !== null) {
1327
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1328
                $contentUpdateStruct->initialLanguageCode
1329
            );
1330
        } else {
1331
            $contentUpdateStruct->initialLanguageCode = $content->contentInfo->mainLanguageCode;
1332
        }
1333
1334
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1335
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1336
1337 View Code Duplication
        foreach ($contentUpdateStruct->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...
1338
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1339
                continue;
1340
            }
1341
1342
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1343
                $field->languageCode
1344
            );
1345
            $languageCodes[$field->languageCode] = true;
1346
        }
1347
1348
        return array_keys($languageCodes);
1349
    }
1350
1351
    /**
1352
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1353
     *
1354
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1355
     *                                                                          or value is set for non-translatable field in language
1356
     *                                                                          other than main
1357
     *
1358
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1359
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1360
     * @param string $mainLanguageCode
1361
     *
1362
     * @return array
1363
     */
1364
    protected function mapFieldsForUpdate(
1365
        APIContentUpdateStruct $contentUpdateStruct,
1366
        ContentType $contentType,
1367
        $mainLanguageCode
1368
    ) {
1369
        $fields = array();
1370
1371
        foreach ($contentUpdateStruct->fields as $field) {
1372
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1373
1374
            if ($fieldDefinition === null) {
1375
                throw new ContentValidationException(
1376
                    "Field definition '%identifier%' does not exist in given ContentType",
1377
                    ['%identifier%' => $field->fieldDefIdentifier]
1378
                );
1379
            }
1380
1381
            if ($field->languageCode === null) {
1382
                if ($fieldDefinition->isTranslatable) {
1383
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1384
                } else {
1385
                    $languageCode = $mainLanguageCode;
1386
                }
1387
                $field = $this->cloneField($field, array('languageCode' => $languageCode));
1388
            }
1389
1390 View Code Duplication
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
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...
1391
                throw new ContentValidationException(
1392
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1393
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1394
                );
1395
            }
1396
1397
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1398
        }
1399
1400
        return $fields;
1401
    }
1402
1403
    /**
1404
     * Publishes a content version.
1405
     *
1406
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to publish this version
1407
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1408
     *
1409
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1410
     *
1411
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1412
     */
1413
    public function publishVersion(APIVersionInfo $versionInfo)
1414
    {
1415
        $content = $this->internalLoadContent(
1416
            $versionInfo->contentInfo->id,
1417
            null,
1418
            $versionInfo->versionNo
1419
        );
1420
1421
        if (!$content->getVersionInfo()->getContentInfo()->published) {
1422
            if (!$this->repository->canUser('content', 'create', $content)) {
1423
                throw new UnauthorizedException('content', 'create', array('contentId' => $content->id));
1424
            }
1425
        } elseif (!$this->repository->canUser('content', 'edit', $content)) {
1426
            throw new UnauthorizedException('content', 'edit', array('contentId' => $content->id));
1427
        }
1428
1429
        $this->repository->beginTransaction();
1430
        try {
1431
            $content = $this->internalPublishVersion($content->getVersionInfo());
1432
            $this->repository->commit();
1433
        } catch (Exception $e) {
1434
            $this->repository->rollback();
1435
            throw $e;
1436
        }
1437
1438
        return $content;
1439
    }
1440
1441
    /**
1442
     * Publishes a content version.
1443
     *
1444
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1445
     *
1446
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1447
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1448
     *
1449
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1450
     */
1451
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1452
    {
1453
        if ($versionInfo->status !== APIVersionInfo::STATUS_DRAFT) {
1454
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1455
        }
1456
1457
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1458
            $publicationDate = time();
1459
        }
1460
1461
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1462
        $metadataUpdateStruct->publicationDate = $publicationDate;
1463
        $metadataUpdateStruct->modificationDate = time();
1464
1465
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1466
            $versionInfo->getContentInfo()->id,
1467
            $versionInfo->versionNo,
1468
            $metadataUpdateStruct
1469
        );
1470
        $content = $this->domainMapper->buildContentDomainObject($spiContent);
1471
1472
        $this->publishUrlAliasesForContent($content);
1473
1474
        return $content;
1475
    }
1476
1477
    /**
1478
     * Removes the given version.
1479
     *
1480
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1481
     *         published state or is the last version of the Content
1482
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1483
     *
1484
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1485
     */
1486
    public function deleteVersion(APIVersionInfo $versionInfo)
1487
    {
1488
        if ($versionInfo->status === APIVersionInfo::STATUS_PUBLISHED) {
1489
            throw new BadStateException(
1490
                '$versionInfo',
1491
                'Version is published and can not be removed'
1492
            );
1493
        }
1494
1495 View Code Duplication
        if (!$this->repository->canUser('content', 'versionremove', $versionInfo)) {
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...
1496
            throw new UnauthorizedException(
1497
                'content',
1498
                'versionremove',
1499
                array('contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo)
1500
            );
1501
        }
1502
1503
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1504
            $versionInfo->contentInfo->id
1505
        );
1506
1507
        if (count($versionList) === 1) {
1508
            throw new BadStateException(
1509
                '$versionInfo',
1510
                'Version is the last version of the Content and can not be removed'
1511
            );
1512
        }
1513
1514
        $this->repository->beginTransaction();
1515
        try {
1516
            $this->persistenceHandler->contentHandler()->deleteVersion(
1517
                $versionInfo->getContentInfo()->id,
1518
                $versionInfo->versionNo
1519
            );
1520
            $this->repository->commit();
1521
        } catch (Exception $e) {
1522
            $this->repository->rollback();
1523
            throw $e;
1524
        }
1525
    }
1526
1527
    /**
1528
     * Loads all versions for the given content.
1529
     *
1530
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1531
     *
1532
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1533
     *
1534
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1535
     */
1536
    public function loadVersions(ContentInfo $contentInfo)
1537
    {
1538
        if (!$this->repository->canUser('content', 'versionread', $contentInfo)) {
1539
            throw new UnauthorizedException('content', 'versionread', array('contentId' => $contentInfo->id));
1540
        }
1541
1542
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id);
1543
1544
        $versions = array();
1545 View Code Duplication
        foreach ($spiVersionInfoList as $spiVersionInfo) {
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...
1546
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1547
            if (!$this->repository->canUser('content', 'versionread', $versionInfo)) {
1548
                throw new UnauthorizedException('content', 'versionread', array('versionId' => $versionInfo->id));
1549
            }
1550
1551
            $versions[] = $versionInfo;
1552
        }
1553
1554
        usort(
1555
            $versions,
1556
            function (VersionInfo $a, VersionInfo $b) {
1557
                if ($a->creationDate->getTimestamp() === $b->creationDate->getTimestamp()) {
0 ignored issues
show
Documentation introduced by
The property $creationDate is declared protected in eZ\Publish\API\Repositor...ues\Content\VersionInfo. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1558
                    return 0;
1559
                }
1560
1561
                return ($a->creationDate->getTimestamp() < $b->creationDate->getTimestamp()) ? -1 : 1;
0 ignored issues
show
Documentation introduced by
The property $creationDate is declared protected in eZ\Publish\API\Repositor...ues\Content\VersionInfo. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1562
            }
1563
        );
1564
1565
        return $versions;
1566
    }
1567
1568
    /**
1569
     * Copies the content to a new location. If no version is given,
1570
     * all versions are copied, otherwise only the given version.
1571
     *
1572
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1573
     *
1574
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1575
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1576
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1577
     *
1578
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1579
     */
1580
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, APIVersionInfo $versionInfo = null)
1581
    {
1582 View Code Duplication
        if (!$this->repository->canUser('content', 'create', $contentInfo, $destinationLocationCreateStruct)) {
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...
1583
            throw new UnauthorizedException(
1584
                'content',
1585
                'create',
1586
                array(
1587
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1588
                    'sectionId' => $contentInfo->sectionId,
1589
                )
1590
            );
1591
        }
1592
1593
        $defaultObjectStates = $this->getDefaultObjectStates();
1594
1595
        $this->repository->beginTransaction();
1596
        try {
1597
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1598
                $contentInfo->id,
1599
                $versionInfo ? $versionInfo->versionNo : null
1600
            );
1601
1602
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1603
                $this->persistenceHandler->objectStateHandler()->setContentState(
1604
                    $spiContent->versionInfo->contentInfo->id,
1605
                    $objectStateGroupId,
1606
                    $objectState->id
1607
                );
1608
            }
1609
1610
            $content = $this->internalPublishVersion(
1611
                $this->domainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1612
                $spiContent->versionInfo->creationDate
1613
            );
1614
1615
            $this->repository->getLocationService()->createLocation(
1616
                $content->getVersionInfo()->getContentInfo(),
1617
                $destinationLocationCreateStruct
1618
            );
1619
            $this->repository->commit();
1620
        } catch (Exception $e) {
1621
            $this->repository->rollback();
1622
            throw $e;
1623
        }
1624
1625
        return $this->internalLoadContent($content->id);
1626
    }
1627
1628
    /**
1629
     * Loads all outgoing relations for the given version.
1630
     *
1631
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1632
     *
1633
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1634
     *
1635
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1636
     */
1637
    public function loadRelations(APIVersionInfo $versionInfo)
1638
    {
1639
        if ($versionInfo->status === APIVersionInfo::STATUS_PUBLISHED) {
1640
            $function = 'read';
1641
        } else {
1642
            $function = 'versionread';
1643
        }
1644
1645
        if (!$this->repository->canUser('content', $function, $versionInfo)) {
1646
            throw new UnauthorizedException('content', $function);
1647
        }
1648
1649
        $contentInfo = $versionInfo->getContentInfo();
1650
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1651
            $contentInfo->id,
1652
            $versionInfo->versionNo
1653
        );
1654
1655
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
1656
        $relations = array();
1657 View Code Duplication
        foreach ($spiRelations as $spiRelation) {
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...
1658
            $destinationContentInfo = $this->internalLoadContentInfo($spiRelation->destinationContentId);
1659
            if (!$this->repository->canUser('content', 'read', $destinationContentInfo)) {
1660
                continue;
1661
            }
1662
1663
            $relations[] = $this->domainMapper->buildRelationDomainObject(
1664
                $spiRelation,
1665
                $contentInfo,
1666
                $destinationContentInfo
1667
            );
1668
        }
1669
1670
        return $relations;
1671
    }
1672
1673
    /**
1674
     * Loads all incoming relations for a content object.
1675
     *
1676
     * The relations come only from published versions of the source content objects
1677
     *
1678
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1679
     *
1680
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1681
     *
1682
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1683
     */
1684
    public function loadReverseRelations(ContentInfo $contentInfo)
1685
    {
1686
        if ($this->repository->hasAccess('content', 'reverserelatedlist') !== true) {
1687
            throw new UnauthorizedException('content', 'reverserelatedlist', array('contentId' => $contentInfo->id));
1688
        }
1689
1690
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
1691
            $contentInfo->id
1692
        );
1693
1694
        $returnArray = array();
1695 View Code Duplication
        foreach ($spiRelations as $spiRelation) {
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...
1696
            $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1697
            if (!$this->repository->canUser('content', 'read', $sourceContentInfo)) {
1698
                continue;
1699
            }
1700
1701
            $returnArray[] = $this->domainMapper->buildRelationDomainObject(
1702
                $spiRelation,
1703
                $sourceContentInfo,
1704
                $contentInfo
1705
            );
1706
        }
1707
1708
        return $returnArray;
1709
    }
1710
1711
    /**
1712
     * Adds a relation of type common.
1713
     *
1714
     * The source of the relation is the content and version
1715
     * referenced by $versionInfo.
1716
     *
1717
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
1718
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1719
     *
1720
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
1721
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
1722
     *
1723
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
1724
     */
1725
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
1726
    {
1727
        $sourceVersion = $this->loadVersionInfoById(
1728
            $sourceVersion->contentInfo->id,
1729
            $sourceVersion->versionNo
1730
        );
1731
1732
        if ($sourceVersion->status !== APIVersionInfo::STATUS_DRAFT) {
1733
            throw new BadStateException(
1734
                '$sourceVersion',
1735
                'Relations of type common can only be added to versions of status draft'
1736
            );
1737
        }
1738
1739
        if (!$this->repository->canUser('content', 'edit', $sourceVersion)) {
1740
            throw new UnauthorizedException('content', 'edit', array('contentId' => $sourceVersion->contentInfo->id));
1741
        }
1742
1743
        $sourceContentInfo = $sourceVersion->getContentInfo();
1744
1745
        $this->repository->beginTransaction();
1746
        try {
1747
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
1748
                new SPIRelationCreateStruct(
1749
                    array(
1750
                        'sourceContentId' => $sourceContentInfo->id,
1751
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
1752
                        'sourceFieldDefinitionId' => null,
1753
                        'destinationContentId' => $destinationContent->id,
1754
                        'type' => APIRelation::COMMON,
1755
                    )
1756
                )
1757
            );
1758
            $this->repository->commit();
1759
        } catch (Exception $e) {
1760
            $this->repository->rollback();
1761
            throw $e;
1762
        }
1763
1764
        return $this->domainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
1765
    }
1766
1767
    /**
1768
     * Removes a relation of type COMMON from a draft.
1769
     *
1770
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
1771
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1772
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
1773
     *
1774
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
1775
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
1776
     */
1777
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
1778
    {
1779
        $sourceVersion = $this->loadVersionInfoById(
1780
            $sourceVersion->contentInfo->id,
1781
            $sourceVersion->versionNo
1782
        );
1783
1784
        if ($sourceVersion->status !== APIVersionInfo::STATUS_DRAFT) {
1785
            throw new BadStateException(
1786
                '$sourceVersion',
1787
                'Relations of type common can only be removed from versions of status draft'
1788
            );
1789
        }
1790
1791
        if (!$this->repository->canUser('content', 'edit', $sourceVersion)) {
1792
            throw new UnauthorizedException('content', 'edit', array('contentId' => $sourceVersion->contentInfo->id));
1793
        }
1794
1795
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1796
            $sourceVersion->getContentInfo()->id,
1797
            $sourceVersion->versionNo,
1798
            APIRelation::COMMON
1799
        );
1800
1801
        if (empty($spiRelations)) {
1802
            throw new InvalidArgumentException(
1803
                '$sourceVersion',
1804
                'There are no relations of type COMMON for the given destination'
1805
            );
1806
        }
1807
1808
        // there should be only one relation of type COMMON for each destination,
1809
        // but in case there were ever more then one, we will remove them all
1810
        // @todo: alternatively, throw BadStateException?
1811
        $this->repository->beginTransaction();
1812
        try {
1813
            foreach ($spiRelations as $spiRelation) {
1814
                if ($spiRelation->destinationContentId == $destinationContent->id) {
1815
                    $this->persistenceHandler->contentHandler()->removeRelation(
1816
                        $spiRelation->id,
1817
                        APIRelation::COMMON
1818
                    );
1819
                }
1820
            }
1821
            $this->repository->commit();
1822
        } catch (Exception $e) {
1823
            $this->repository->rollback();
1824
            throw $e;
1825
        }
1826
    }
1827
1828
    /**
1829
     * Adds translation information to the content object.
1830
     *
1831
     * @example Examples/translation_5x.php
1832
     *
1833
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed add a translation info
1834
     *
1835
     * @param \eZ\Publish\API\Repository\Values\Content\TranslationInfo $translationInfo
1836
     *
1837
     * @since 5.0
1838
     */
1839
    public function addTranslationInfo(TranslationInfo $translationInfo)
1840
    {
1841
        throw new NotImplementedException(__METHOD__);
1842
    }
1843
1844
    /**
1845
     * lists the translations done on this content object.
1846
     *
1847
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed read translation infos
1848
     *
1849
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1850
     * @param array $filter
1851
     *
1852
     * @todo TBD - filter by source version destination version and languages
1853
     *
1854
     * @return \eZ\Publish\API\Repository\Values\Content\TranslationInfo[]
1855
     *
1856
     * @since 5.0
1857
     */
1858
    public function loadTranslationInfos(ContentInfo $contentInfo, array $filter = array())
1859
    {
1860
        throw new NotImplementedException(__METHOD__);
1861
    }
1862
1863
    /**
1864
     * Instantiates a new content create struct object.
1865
     *
1866
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1867
     * @param string $mainLanguageCode
1868
     *
1869
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
1870
     */
1871
    public function newContentCreateStruct(ContentType $contentType, $mainLanguageCode)
1872
    {
1873
        return new ContentCreateStruct(
1874
            array(
1875
                'contentType' => $contentType,
1876
                'mainLanguageCode' => $mainLanguageCode,
1877
            )
1878
        );
1879
    }
1880
1881
    /**
1882
     * Instantiates a new content meta data update struct.
1883
     *
1884
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
1885
     */
1886
    public function newContentMetadataUpdateStruct()
1887
    {
1888
        return new ContentMetadataUpdateStruct();
1889
    }
1890
1891
    /**
1892
     * Instantiates a new content update struct.
1893
     *
1894
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
1895
     */
1896
    public function newContentUpdateStruct()
1897
    {
1898
        return new ContentUpdateStruct();
1899
    }
1900
1901
    /**
1902
     * Instantiates a new TranslationInfo object.
1903
     *
1904
     * @return \eZ\Publish\API\Repository\Values\Content\TranslationInfo
1905
     */
1906
    public function newTranslationInfo()
1907
    {
1908
        return new TranslationInfo();
1909
    }
1910
1911
    /**
1912
     * Instantiates a Translation object.
1913
     *
1914
     * @return \eZ\Publish\API\Repository\Values\Content\TranslationValues
1915
     */
1916
    public function newTranslationValues()
1917
    {
1918
        return new TranslationValues();
1919
    }
1920
}
1921