1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Kaliop\eZMigrationBundle\Core\Executor; |
4
|
|
|
|
5
|
|
|
use eZ\Publish\API\Repository\Values\ContentType\ContentType; |
6
|
|
|
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition; |
7
|
|
|
use eZ\Publish\API\Repository\Values\Content\Location; |
8
|
|
|
use eZ\Publish\API\Repository\Values\Content\Content; |
9
|
|
|
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct; |
10
|
|
|
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct; |
11
|
|
|
use eZ\Publish\Core\Base\Exceptions\NotFoundException; |
12
|
|
|
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection; |
13
|
|
|
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface; |
14
|
|
|
use Kaliop\eZMigrationBundle\Core\FieldHandlerManager; |
15
|
|
|
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher; |
16
|
|
|
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher; |
17
|
|
|
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher; |
18
|
|
|
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher; |
19
|
|
|
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher; |
20
|
|
|
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter; |
21
|
|
|
use JmesPath\Env as JmesPath; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Handles content migrations. |
25
|
|
|
* |
26
|
|
|
* @todo add support for updating of content metadata |
27
|
|
|
*/ |
28
|
|
|
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface |
29
|
|
|
{ |
30
|
|
|
protected $supportedStepTypes = array('content'); |
31
|
|
|
protected $supportedActions = array('create', 'load', 'update', 'delete'); |
32
|
|
|
|
33
|
|
|
protected $contentMatcher; |
34
|
|
|
protected $sectionMatcher; |
35
|
|
|
protected $userMatcher; |
36
|
|
|
protected $objectStateMatcher; |
37
|
|
|
protected $objectStateGroupMatcher; |
38
|
|
|
protected $fieldHandlerManager; |
39
|
|
|
protected $locationManager; |
40
|
|
|
protected $sortConverter; |
41
|
|
|
|
42
|
|
|
// these are not exported when generating a migration |
43
|
|
|
protected $ignoredStateGroupIdentifiers = array('ez_lock'); |
44
|
|
|
|
45
|
80 |
View Code Duplication |
public function __construct( |
|
|
|
|
46
|
|
|
ContentMatcher $contentMatcher, |
47
|
|
|
SectionMatcher $sectionMatcher, |
48
|
|
|
UserMatcher $userMatcher, |
49
|
|
|
ObjectStateMatcher $objectStateMatcher, |
50
|
|
|
ObjectStateGroupMatcher $objectStateGroupMatcher, |
51
|
|
|
FieldHandlerManager $fieldHandlerManager, |
52
|
|
|
LocationManager $locationManager, |
53
|
|
|
SortConverter $sortConverter |
54
|
|
|
) { |
55
|
80 |
|
$this->contentMatcher = $contentMatcher; |
56
|
80 |
|
$this->sectionMatcher = $sectionMatcher; |
57
|
80 |
|
$this->userMatcher = $userMatcher; |
58
|
80 |
|
$this->objectStateMatcher = $objectStateMatcher; |
59
|
80 |
|
$this->objectStateGroupMatcher = $objectStateGroupMatcher; |
60
|
80 |
|
$this->fieldHandlerManager = $fieldHandlerManager; |
61
|
80 |
|
$this->locationManager = $locationManager; |
62
|
80 |
|
$this->sortConverter = $sortConverter; |
63
|
80 |
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Handles the content create migration action type |
67
|
|
|
*/ |
68
|
|
|
protected function create($step) |
69
|
|
|
{ |
70
|
|
|
$contentService = $this->repository->getContentService(); |
71
|
|
|
$locationService = $this->repository->getLocationService(); |
72
|
|
|
$contentTypeService = $this->repository->getContentTypeService(); |
73
|
|
|
|
74
|
|
|
$contentTypeIdentifier = $step->dsl['content_type']; |
75
|
|
|
$contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier); |
76
|
|
|
/// @todo use a contenttypematcher |
77
|
|
|
$contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier); |
78
|
|
|
|
79
|
|
|
$contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step)); |
80
|
|
|
|
81
|
|
|
$this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step); |
82
|
|
|
|
83
|
|
|
if (isset($step->dsl['always_available'])) { |
84
|
|
|
$contentCreateStruct->alwaysAvailable = $step->dsl['always_available']; |
85
|
|
|
} else { |
86
|
|
|
// Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged, |
87
|
|
|
// but we strive to support old eZ kernel versions as well... |
88
|
|
|
$contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
if (isset($step->dsl['remote_id'])) { |
92
|
|
|
$contentCreateStruct->remoteId = $step->dsl['remote_id']; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
View Code Duplication |
if (isset($step->dsl['section'])) { |
|
|
|
|
96
|
|
|
$sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']); |
97
|
|
|
$section = $this->sectionMatcher->matchOneByKey($sectionKey); |
98
|
|
|
$contentCreateStruct->sectionId = $section->id; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
View Code Duplication |
if (isset($step->dsl['owner'])) { |
|
|
|
|
102
|
|
|
$owner = $this->getUser($step->dsl['owner']); |
103
|
|
|
$contentCreateStruct->ownerId = $owner->id; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
// This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version. |
107
|
|
|
// We allow it, hoping that nothing gets broken because of it |
108
|
|
|
if (isset($step->dsl['version_creator'])) { |
109
|
|
|
$realContentOwnerId = $contentCreateStruct->ownerId; |
110
|
|
|
if ($realContentOwnerId == null) { |
111
|
|
|
$realContentOwnerId = $this->repository->getCurrentUser()->id; |
112
|
|
|
} |
113
|
|
|
$versionCreator = $this->getUser($step->dsl['version_creator']); |
114
|
|
|
$contentCreateStruct->ownerId = $versionCreator->id; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
if (isset($step->dsl['modification_date'])) { |
118
|
|
|
$contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// instantiate a location create struct from the parent location: |
122
|
|
|
// BC |
123
|
|
|
$locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : ( |
124
|
|
|
isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null |
125
|
|
|
); |
126
|
|
|
// 1st resolve references |
127
|
|
|
$locationId = $this->referenceResolver->resolveReference($locationId); |
128
|
|
|
// 2nd allow to specify the location via remote_id |
129
|
|
|
$locationId = $this->locationManager->matchLocationByKey($locationId)->id; |
130
|
|
|
$locationCreateStruct = $locationService->newLocationCreateStruct($locationId); |
131
|
|
|
|
132
|
|
|
if (isset($step->dsl['location_remote_id'])) { |
133
|
|
|
$locationCreateStruct->remoteId = $step->dsl['location_remote_id']; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
if (isset($step->dsl['priority'])) { |
137
|
|
|
$locationCreateStruct->priority = $step->dsl['priority']; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
if (isset($step->dsl['is_hidden'])) { |
141
|
|
|
$locationCreateStruct->hidden = $step->dsl['is_hidden']; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
if (isset($step->dsl['sort_field'])) { |
145
|
|
|
$locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']); |
146
|
|
|
} else { |
147
|
|
|
$locationCreateStruct->sortField = $contentType->defaultSortField; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
if (isset($step->dsl['sort_order'])) { |
151
|
|
|
$locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']); |
152
|
|
|
} else { |
153
|
|
|
$locationCreateStruct->sortOrder = $contentType->defaultSortOrder; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$locations = array($locationCreateStruct); |
157
|
|
|
|
158
|
|
|
// BC |
159
|
|
|
$other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : ( |
160
|
|
|
isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null |
161
|
|
|
); |
162
|
|
|
if (isset($other_locations)) { |
163
|
|
|
foreach ($other_locations as $locationId) { |
164
|
|
|
$locationId = $this->referenceResolver->resolveReference($locationId); |
165
|
|
|
$locationId = $this->locationManager->matchLocationByKey($locationId)->id; |
166
|
|
|
$secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId); |
167
|
|
|
array_push($locations, $secondaryLocationCreateStruct); |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
// create a draft using the content and location create struct and publish it |
172
|
|
|
$draft = $contentService->createContent($contentCreateStruct, $locations); |
173
|
|
|
$content = $contentService->publishVersion($draft->versionInfo); |
174
|
|
|
|
175
|
|
|
if (isset($step->dsl['object_states'])) { |
176
|
|
|
$this->setObjectStates($content, $step->dsl['object_states']); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// 2nd part of the hack: re-set the content owner to its intended value |
180
|
|
|
if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) { |
181
|
|
|
$contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct(); |
182
|
|
|
|
183
|
|
|
if (isset($step->dsl['version_creator'])) { |
184
|
|
|
$contentMetaDataUpdateStruct->ownerId = $realContentOwnerId; |
|
|
|
|
185
|
|
|
} |
186
|
|
|
if (isset($step->dsl['publication_date'])) { |
187
|
|
|
$contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']); |
188
|
|
|
} |
189
|
|
|
// we have to do this to make sure we preserve the custom modification date |
190
|
|
|
if (isset($step->dsl['modification_date'])) { |
191
|
|
|
$contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
$this->setReferences($content, $step); |
|
|
|
|
198
|
|
|
|
199
|
|
|
return $content; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
protected function load($step) |
203
|
|
|
{ |
204
|
|
|
$contentCollection = $this->matchContents('load', $step); |
205
|
|
|
|
206
|
|
|
$this->setReferences($contentCollection, $step); |
207
|
|
|
|
208
|
|
|
return $contentCollection; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Handles the content update migration action type |
213
|
|
|
* |
214
|
|
|
* @todo handle updating of more metadata fields |
215
|
|
|
*/ |
216
|
|
|
protected function update($step) |
217
|
|
|
{ |
218
|
|
|
$contentService = $this->repository->getContentService(); |
219
|
|
|
$contentTypeService = $this->repository->getContentTypeService(); |
220
|
|
|
|
221
|
|
|
$contentCollection = $this->matchContents('update', $step); |
222
|
|
|
|
223
|
|
|
if (count($contentCollection) > 1 && isset($step->dsl['references'])) { |
224
|
|
|
throw new \Exception("Can not execute Content update because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches"); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if (count($contentCollection) > 1 && isset($step->dsl['main_location'])) { |
228
|
|
|
throw new \Exception("Can not execute Content update because multiple contents match, and a main_location section is specified in the dsl. References can be set when only 1 content matches"); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$contentType = array(); |
232
|
|
|
|
233
|
|
|
foreach ($contentCollection as $key => $content) { |
234
|
|
|
$contentInfo = $content->contentInfo; |
235
|
|
|
|
236
|
|
|
if (!isset($contentType[$contentInfo->contentTypeId])) { |
237
|
|
|
$contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) { |
241
|
|
|
$contentUpdateStruct = $contentService->newContentUpdateStruct(); |
242
|
|
|
|
243
|
|
|
if (isset($step->dsl['attributes'])) { |
244
|
|
|
$this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
$versionCreator = null; |
248
|
|
|
if (isset($step->dsl['version_creator'])) { |
249
|
|
|
$versionCreator = $this->getUser($step->dsl['version_creator']); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$draft = $contentService->createContentDraft($contentInfo, null, $versionCreator); |
253
|
|
|
$contentService->updateContent($draft->versionInfo, $contentUpdateStruct); |
254
|
|
|
$content = $contentService->publishVersion($draft->versionInfo); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
if (isset($step->dsl['always_available']) || |
258
|
|
|
isset($step->dsl['new_remote_id']) || |
259
|
|
|
isset($step->dsl['owner']) || |
260
|
|
|
isset($step->dsl['modification_date']) || |
261
|
|
|
isset($step->dsl['publication_date'])) { |
262
|
|
|
|
263
|
|
|
$contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct(); |
264
|
|
|
|
265
|
|
|
if (isset($step->dsl['always_available'])) { |
266
|
|
|
$contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available']; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
if (isset($step->dsl['new_remote_id'])) { |
270
|
|
|
$contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id']; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
View Code Duplication |
if (isset($step->dsl['owner'])) { |
|
|
|
|
274
|
|
|
$owner = $this->getUser($step->dsl['owner']); |
275
|
|
|
$contentMetaDataUpdateStruct->ownerId = $owner->id; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
if (isset($step->dsl['modification_date'])) { |
279
|
|
|
$contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
if (isset($step->dsl['publication_date'])) { |
283
|
|
|
$contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
$content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
if (isset($step->dsl['section'])) { |
290
|
|
|
$this->setSection($content, $step->dsl['section']); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
if (isset($step->dsl['object_states'])) { |
294
|
|
|
$this->setObjectStates($content, $step->dsl['object_states']); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
if (isset($step->dsl['main_location'])) { |
298
|
|
|
$this->setMainLocation($content, $step->dsl['main_location']); |
299
|
|
|
|
300
|
|
|
} |
301
|
|
|
$contentCollection[$key] = $content; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$this->setReferences($contentCollection, $step); |
305
|
|
|
|
306
|
|
|
return $contentCollection; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Handles the content delete migration action type |
311
|
|
|
*/ |
312
|
|
View Code Duplication |
protected function delete($step) |
|
|
|
|
313
|
|
|
{ |
314
|
|
|
$contentCollection = $this->matchContents('delete', $step); |
315
|
|
|
|
316
|
|
|
$this->setReferences($contentCollection, $step); |
317
|
|
|
|
318
|
|
|
$contentService = $this->repository->getContentService(); |
319
|
|
|
|
320
|
|
|
foreach ($contentCollection as $content) { |
321
|
|
|
try { |
322
|
|
|
$contentService->deleteContent($content->contentInfo); |
323
|
|
|
} catch (NotFoundException $e) { |
324
|
|
|
// Someone else (or even us, by virtue of location tree?) removed the content which we found just a |
325
|
|
|
// second ago. We can safely ignore this |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
return $contentCollection; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param string $action |
334
|
|
|
* @return ContentCollection |
335
|
|
|
* @throws \Exception |
336
|
|
|
*/ |
337
|
|
|
protected function matchContents($action, $step) |
338
|
|
|
{ |
339
|
|
View Code Duplication |
if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) { |
|
|
|
|
340
|
|
|
throw new \Exception("The id or remote id of an object or a match condition is required to $action a content"); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
// Backwards compat |
344
|
|
|
|
345
|
|
View Code Duplication |
if (isset($step->dsl['match'])) { |
|
|
|
|
346
|
|
|
$match = $step->dsl['match']; |
347
|
|
|
} else { |
348
|
|
|
if (isset($step->dsl['object_id'])) { |
349
|
|
|
$match = array('content_id' => $step->dsl['object_id']); |
350
|
|
|
} elseif (isset($step->dsl['remote_id'])) { |
351
|
|
|
$match = array('content_remote_id' => $step->dsl['remote_id']); |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
// convert the references passed in the match |
356
|
|
|
$match = $this->resolveReferencesRecursively($match); |
|
|
|
|
357
|
|
|
|
358
|
|
|
$offset = isset($step->dsl['match_offset']) ? $this->referenceResolver->resolveReference($step->dsl['match_offset']) : 0; |
359
|
|
|
$limit = isset($step->dsl['match_limit']) ? $this->referenceResolver->resolveReference($step->dsl['match_limit']) : 0; |
360
|
|
|
$sort = isset($step->dsl['match_sort']) ? $this->referenceResolver->resolveReference($step->dsl['match_sort']) : array(); |
361
|
|
|
|
362
|
|
|
return $this->contentMatcher->match($match, $sort, $offset, $limit); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* @param Content $content |
367
|
|
|
* @param array $references the definitions of the references to set |
368
|
|
|
* @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute |
369
|
|
|
* @return array key: the reference names, values: the reference values |
370
|
|
|
*/ |
371
|
|
|
protected function getReferencesValues($content, array $references, $step) |
372
|
|
|
{ |
373
|
|
|
$refs = array(); |
374
|
|
|
|
375
|
|
|
foreach ($references as $reference) { |
376
|
|
|
|
377
|
|
|
switch ($reference['attribute']) { |
378
|
|
|
case 'object_id': |
379
|
|
|
case 'content_id': |
380
|
|
|
case 'id': |
381
|
|
|
$value = $content->id; |
382
|
|
|
break; |
383
|
|
|
case 'remote_id': |
384
|
|
|
case 'content_remote_id': |
385
|
|
|
$value = $content->contentInfo->remoteId; |
386
|
|
|
break; |
387
|
|
|
case 'always_available': |
388
|
|
|
$value = $content->contentInfo->alwaysAvailable; |
389
|
|
|
break; |
390
|
|
|
case 'content_type_id': |
391
|
|
|
$value = $content->contentInfo->contentTypeId; |
392
|
|
|
break; |
393
|
|
|
case 'content_type_identifier': |
394
|
|
|
$contentTypeService = $this->repository->getContentTypeService(); |
395
|
|
|
$value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier; |
396
|
|
|
break; |
397
|
|
|
case 'current_version': |
398
|
|
|
case 'current_version_no': |
399
|
|
|
$value = $content->contentInfo->currentVersionNo; |
400
|
|
|
break; |
401
|
|
|
case 'location_id': |
402
|
|
|
case 'main_location_id': |
403
|
|
|
$value = $content->contentInfo->mainLocationId; |
404
|
|
|
break; |
405
|
|
|
case 'main_language_code': |
406
|
|
|
$value = $content->contentInfo->mainLanguageCode; |
407
|
|
|
break; |
408
|
|
|
case 'modification_date': |
409
|
|
|
$value = $content->contentInfo->modificationDate->getTimestamp(); |
410
|
|
|
break; |
411
|
|
|
case 'name': |
412
|
|
|
$value = $content->contentInfo->name; |
413
|
|
|
break; |
414
|
|
|
case 'owner_id': |
415
|
|
|
$value = $content->contentInfo->ownerId; |
416
|
|
|
break; |
417
|
|
|
case 'path': |
418
|
|
|
$locationService = $this->repository->getLocationService(); |
419
|
|
|
$value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString; |
420
|
|
|
break; |
421
|
|
|
case 'publication_date': |
422
|
|
|
$value = $content->contentInfo->publishedDate->getTimestamp(); |
423
|
|
|
break; |
424
|
|
|
case 'section_id': |
425
|
|
|
$value = $content->contentInfo->sectionId; |
426
|
|
|
break; |
427
|
|
|
case 'section_identifier': |
428
|
|
|
$sectionService = $this->repository->getSectionService(); |
429
|
|
|
$value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier; |
430
|
|
|
break; |
431
|
|
|
case 'version_count': |
432
|
|
|
$contentService = $this->repository->getContentService(); |
433
|
|
|
$value = count($contentService->loadVersions($content->contentInfo)); |
434
|
|
|
break; |
435
|
|
|
default: |
436
|
|
|
if (strpos($reference['attribute'], 'object_state.') === 0) { |
437
|
|
|
$stateGroupKey = substr($reference['attribute'], 13); |
438
|
|
|
$stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey); |
439
|
|
|
$value = $stateGroupKey . '/' . $this->repository->getObjectStateService()-> |
440
|
|
|
getContentState($content->contentInfo, $stateGroup)->identifier; |
441
|
|
|
break; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
// allow to get the value of fields as well as their sub-parts |
445
|
|
|
if (strpos($reference['attribute'], 'attributes.') === 0) { |
446
|
|
|
$contentType = $this->repository->getContentTypeService()->loadContentType( |
447
|
|
|
$content->contentInfo->contentTypeId |
448
|
|
|
); |
449
|
|
|
$parts = explode('.', $reference['attribute']); |
450
|
|
|
// totally not sure if this list of special chars is correct for what could follow a jmespath identifier... |
451
|
|
|
// also what about quoted strings? |
452
|
|
|
$fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]); |
453
|
|
|
$field = $content->getField($fieldIdentifier); |
|
|
|
|
454
|
|
|
$fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); |
455
|
|
|
$hashValue = $this->fieldHandlerManager->fieldValueToHash( |
456
|
|
|
$fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value |
457
|
|
|
); |
458
|
|
|
if (is_array($hashValue)) { |
459
|
|
View Code Duplication |
if (count($parts) == 2 && $fieldIdentifier === $parts[1]) { |
|
|
|
|
460
|
|
|
throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value'); |
461
|
|
|
} |
462
|
|
|
$value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue)); |
463
|
|
|
} else { |
464
|
|
|
if (count($parts) > 2) { |
465
|
|
|
throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value'); |
466
|
|
|
} |
467
|
|
|
$value = $hashValue; |
468
|
|
|
} |
469
|
|
|
break; |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
$refs[$reference['identifier']] = $value; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
return $refs; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* @param array $matchCondition |
483
|
|
|
* @param string $mode |
484
|
|
|
* @param array $context |
485
|
|
|
* @throws \Exception |
486
|
|
|
* @return array |
487
|
|
|
* |
488
|
|
|
* @todo add support for dumping all object languages |
489
|
|
|
* @todo add 2ndary locations when in 'update' mode |
490
|
|
|
* @todo add dumping of sort_field and sort_order for 2ndary locations |
491
|
|
|
*/ |
492
|
|
|
public function generateMigration(array $matchCondition, $mode, array $context = array()) |
493
|
|
|
{ |
494
|
|
|
$previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); |
495
|
|
|
$contentCollection = $this->contentMatcher->match($matchCondition); |
496
|
|
|
$data = array(); |
497
|
|
|
|
498
|
|
|
/** @var \eZ\Publish\API\Repository\Values\Content\Content $content */ |
499
|
|
|
foreach ($contentCollection as $content) { |
500
|
|
|
|
501
|
|
|
$location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId); |
502
|
|
|
$contentType = $this->repository->getContentTypeService()->loadContentType( |
503
|
|
|
$content->contentInfo->contentTypeId |
504
|
|
|
); |
505
|
|
|
|
506
|
|
|
$contentData = array( |
507
|
|
|
'type' => reset($this->supportedStepTypes), |
508
|
|
|
'mode' => $mode |
509
|
|
|
); |
510
|
|
|
|
511
|
|
|
switch ($mode) { |
512
|
|
|
case 'create': |
513
|
|
|
$contentData = array_merge( |
514
|
|
|
$contentData, |
515
|
|
|
array( |
516
|
|
|
'content_type' => $contentType->identifier, |
517
|
|
|
'parent_location' => $location->parentLocationId, |
518
|
|
|
'priority' => $location->priority, |
519
|
|
|
'is_hidden' => $location->invisible, |
520
|
|
|
'sort_field' => $this->sortConverter->sortField2Hash($location->sortField), |
521
|
|
|
'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder), |
522
|
|
|
'remote_id' => $content->contentInfo->remoteId, |
523
|
|
|
'location_remote_id' => $location->remoteId, |
524
|
|
|
'section' => $content->contentInfo->sectionId, |
525
|
|
|
'object_states' => $this->getObjectStates($content), |
526
|
|
|
) |
527
|
|
|
); |
528
|
|
|
$locationService = $this->repository->getLocationService(); |
529
|
|
|
/// @todo for accurate replication, we should express the addinfg of 2ndary locatins as separate steps, and copy over visibility, priority etc |
530
|
|
|
$locations = $locationService->loadLocations($content->contentInfo); |
531
|
|
|
if (count($locations) > 1) { |
532
|
|
|
$otherParentLocations = array(); |
533
|
|
|
foreach($locations as $otherLocation) { |
534
|
|
|
if ($otherLocation->id != $location->id) { |
535
|
|
|
$otherParentLocations[] = $otherLocation->parentLocationId; |
536
|
|
|
} |
537
|
|
|
} |
538
|
|
|
$contentData['other_parent_locations'] = $otherParentLocations; |
539
|
|
|
} |
540
|
|
|
break; |
541
|
|
|
case 'update': |
542
|
|
|
$contentData = array_merge( |
543
|
|
|
$contentData, |
544
|
|
|
array( |
545
|
|
|
'match' => array( |
546
|
|
|
ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId |
547
|
|
|
), |
548
|
|
|
'new_remote_id' => $content->contentInfo->remoteId, |
549
|
|
|
'section' => $content->contentInfo->sectionId, |
550
|
|
|
'object_states' => $this->getObjectStates($content), |
551
|
|
|
) |
552
|
|
|
); |
553
|
|
|
break; |
554
|
|
|
case 'delete': |
555
|
|
|
$contentData = array_merge( |
556
|
|
|
$contentData, |
557
|
|
|
array( |
558
|
|
|
'match' => array( |
559
|
|
|
ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId |
560
|
|
|
) |
561
|
|
|
) |
562
|
|
|
); |
563
|
|
|
break; |
564
|
|
|
default: |
565
|
|
|
throw new \Exception("Executor 'content' doesn't support mode '$mode'"); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
if ($mode != 'delete') { |
569
|
|
|
|
570
|
|
|
$attributes = array(); |
571
|
|
|
foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) { |
572
|
|
|
$fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); |
573
|
|
|
$attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash( |
574
|
|
|
$fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value |
575
|
|
|
); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$contentData = array_merge( |
579
|
|
|
$contentData, |
580
|
|
|
array( |
581
|
|
|
'lang' => $this->getLanguageCodeFromContext($context), |
582
|
|
|
'section' => $content->contentInfo->sectionId, |
583
|
|
|
'owner' => $content->contentInfo->ownerId, |
584
|
|
|
'modification_date' => $content->contentInfo->modificationDate->getTimestamp(), |
585
|
|
|
'publication_date' => $content->contentInfo->publishedDate->getTimestamp(), |
586
|
|
|
'always_available' => (bool)$content->contentInfo->alwaysAvailable, |
587
|
|
|
'attributes' => $attributes |
588
|
|
|
) |
589
|
|
|
); |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
$data[] = $contentData; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
$this->loginUser($previousUserId); |
596
|
|
|
return $data; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
/** |
600
|
|
|
* Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings. |
601
|
|
|
* |
602
|
|
|
* @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct |
603
|
|
|
* @param array $fields see description of expected format in code below |
604
|
|
|
* @param ContentType $contentType |
605
|
|
|
* @param $step |
606
|
|
|
* @throws \Exception |
607
|
|
|
*/ |
608
|
|
|
protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step) |
609
|
|
|
{ |
610
|
|
|
$fields = $this->normalizeFieldDefs($fields, $step); |
611
|
|
|
|
612
|
|
|
foreach ($fields as $fieldIdentifier => $fieldLanguages) { |
613
|
|
|
foreach ($fieldLanguages as $language => $fieldValue) { |
614
|
|
|
if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) { |
|
|
|
|
615
|
|
|
throw new \Exception("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'"); |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
$fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier]; |
|
|
|
|
619
|
|
|
$fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context); |
620
|
|
|
$createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language); |
621
|
|
|
} |
622
|
|
|
} |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
/** |
626
|
|
|
* Helper function to accommodate the definition of fields |
627
|
|
|
* - using a legacy DSL version |
628
|
|
|
* - using either single-language or multi-language style |
629
|
|
|
* |
630
|
|
|
* @param array $fields |
631
|
|
|
* @return array |
632
|
|
|
*/ |
633
|
|
|
protected function normalizeFieldDefs($fields, $step) |
634
|
|
|
{ |
635
|
|
|
$convertedFields = []; |
636
|
|
|
$i = 0; |
637
|
|
|
// the 'easy' yml: key = field name, value = value |
638
|
|
|
// deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value ) |
639
|
|
|
foreach ($fields as $key => $field) { |
640
|
|
|
if ($key === $i && is_array($field) && count($field) == 1) { |
641
|
|
|
// each $field is one key value pair |
642
|
|
|
// eg.: $field = array($fieldIdentifier => $fieldValue) |
643
|
|
|
reset($field); |
644
|
|
|
$fieldIdentifier = key($field); |
645
|
|
|
$fieldValue = $field[$fieldIdentifier]; |
646
|
|
|
|
647
|
|
|
$convertedFields[$fieldIdentifier] = $fieldValue; |
648
|
|
|
} else { |
649
|
|
|
$convertedFields[$key] = $field; |
650
|
|
|
} |
651
|
|
|
$i++; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
// transform single-language field defs in multilang ones |
655
|
|
|
if (!$this->hasLanguageCodesAsKeys($convertedFields)) { |
656
|
|
|
$language = $this->getLanguageCode($step); |
657
|
|
|
|
658
|
|
|
foreach ($convertedFields as $fieldIdentifier => $fieldValue) { |
659
|
|
|
$convertedFields[$fieldIdentifier] = array($language => $fieldValue); |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
return $convertedFields; |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
/** |
667
|
|
|
* Checks whether all fields are using multilang syntax ie. a valid language as key. |
668
|
|
|
* |
669
|
|
|
* @param array $fields |
670
|
|
|
* @return bool |
671
|
|
|
*/ |
672
|
|
|
protected function hasLanguageCodesAsKeys(array $fields) |
673
|
|
|
{ |
674
|
|
|
$languageCodes = $this->getContentLanguageCodes(); |
675
|
|
|
|
676
|
|
|
foreach ($fields as $fieldIdentifier => $fieldData) { |
677
|
|
|
if (!is_array($fieldData) || empty($fieldData)) { |
678
|
|
|
return false; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
foreach ($fieldData as $key => $data) { |
682
|
|
|
if (!in_array($key, $languageCodes, true)) { |
683
|
|
|
return false; |
684
|
|
|
} |
685
|
|
|
} |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
return true; |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
/** |
692
|
|
|
* Returns all enabled Languages in the repo. |
693
|
|
|
* @todo move to parent class? |
694
|
|
|
* |
695
|
|
|
* @return string[] |
696
|
|
|
*/ |
697
|
|
|
protected function getContentLanguageCodes() |
698
|
|
|
{ |
699
|
|
|
return array_map( |
700
|
|
|
function($language) { |
701
|
|
|
return $language->languageCode; |
702
|
|
|
}, |
703
|
|
|
array_filter( |
704
|
|
|
$this->repository->getContentLanguageService()->loadLanguages(), |
705
|
|
|
function ($language) { |
706
|
|
|
return $language->enabled; |
707
|
|
|
} |
708
|
|
|
) |
709
|
|
|
); |
710
|
|
|
} |
711
|
|
|
|
712
|
|
View Code Duplication |
protected function setSection(Content $content, $sectionKey) |
|
|
|
|
713
|
|
|
{ |
714
|
|
|
$sectionKey = $this->referenceResolver->resolveReference($sectionKey); |
715
|
|
|
$section = $this->sectionMatcher->matchOneByKey($sectionKey); |
716
|
|
|
|
717
|
|
|
$sectionService = $this->repository->getSectionService(); |
718
|
|
|
$sectionService->assignSection($content->contentInfo, $section); |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
protected function setObjectStates(Content $content, array $stateKeys) |
722
|
|
|
{ |
723
|
|
|
foreach ($stateKeys as $stateKey) { |
724
|
|
|
$stateKey = $this->referenceResolver->resolveReference($stateKey); |
725
|
|
|
/** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */ |
726
|
|
|
$state = $this->objectStateMatcher->matchOneByKey($stateKey); |
727
|
|
|
|
728
|
|
|
$stateService = $this->repository->getObjectStateService(); |
729
|
|
|
$stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state); |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
protected function setMainLocation(Content $content, $locationId) |
734
|
|
|
{ |
735
|
|
|
$locationId = $this->referenceResolver->resolveReference($locationId); |
736
|
|
|
if (is_int($locationId) || ctype_digit($locationId)) { |
737
|
|
|
$location = $this->repository->getLocationService()->loadLocation($locationId); |
738
|
|
|
} else { |
739
|
|
|
$location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId); |
740
|
|
|
} |
741
|
|
|
|
742
|
|
|
if ($location->contentInfo->id != $content->id) { |
743
|
|
|
throw new \Exception("Can not set main location {$location->id} to content {$content->id} as it belongs to another object"); |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
$contentService = $this->repository->getContentService(); |
747
|
|
|
$contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct(); |
748
|
|
|
$contentMetaDataUpdateStruct->mainLocationId = $location->id; |
749
|
|
|
$contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct); |
750
|
|
|
} |
751
|
|
|
|
752
|
|
|
protected function getObjectStates(Content $content) |
753
|
|
|
{ |
754
|
|
|
$states = []; |
755
|
|
|
|
756
|
|
|
$objectStateService = $this->repository->getObjectStateService(); |
757
|
|
|
$groups = $objectStateService->loadObjectStateGroups(); |
758
|
|
|
foreach ($groups as $group) { |
759
|
|
|
if (in_array($group->identifier, $this->ignoredStateGroupIdentifiers)) { |
760
|
|
|
continue; |
761
|
|
|
} |
762
|
|
|
$state = $objectStateService->getContentState($content->contentInfo, $group); |
763
|
|
|
$states[] = $group->identifier . '/' . $state->identifier; |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
return $states; |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Create the field value from the migration definition hash |
771
|
|
|
* |
772
|
|
|
* @param mixed $value |
773
|
|
|
* @param FieldDefinition $fieldDefinition |
774
|
|
|
* @param string $contentTypeIdentifier |
775
|
|
|
* @param array $context |
776
|
|
|
* @throws \InvalidArgumentException |
777
|
|
|
* @return mixed |
778
|
|
|
*/ |
779
|
|
|
protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array()) |
780
|
|
|
{ |
781
|
|
|
$fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; |
782
|
|
|
|
783
|
|
|
if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { |
784
|
|
|
// since we now allow refs to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler... |
785
|
|
|
if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) { |
786
|
|
|
$value = $this->referenceResolver->resolveReference($value); |
787
|
|
|
} |
788
|
|
|
// inject info about the current content type and field into the context |
789
|
|
|
$context['contentTypeIdentifier'] = $contentTypeIdentifier; |
790
|
|
|
$context['fieldIdentifier'] = $fieldDefinition->identifier; |
791
|
|
|
return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context); |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
/** |
798
|
|
|
* Create the field value for a primitive field from the migration definition hash |
799
|
|
|
* |
800
|
|
|
* @param mixed $value |
801
|
|
|
* @param FieldDefinition $fieldDefinition |
802
|
|
|
* @param string $contentTypeIdentifier |
803
|
|
|
* @param array $context |
804
|
|
|
* @throws \InvalidArgumentException |
805
|
|
|
* @return mixed |
806
|
|
|
*/ |
807
|
|
|
protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array()) |
|
|
|
|
808
|
|
|
{ |
809
|
|
|
// booleans were handled here. They are now handled as complextypes |
810
|
|
|
|
811
|
|
|
// q: do we really want this to happen by default on all scalar field values? |
812
|
|
|
// Note: if you want this *not* to happen, register a complex field for your scalar field... |
813
|
|
|
$value = $this->referenceResolver->resolveReference($value); |
814
|
|
|
|
815
|
|
|
return $value; |
816
|
|
|
} |
817
|
|
|
|
818
|
|
|
/** |
819
|
|
|
* Load user using either login, email, id - resolving eventual references |
820
|
|
|
* @param int|string $userKey |
821
|
|
|
* @return \eZ\Publish\API\Repository\Values\User\User |
822
|
|
|
*/ |
823
|
|
|
protected function getUser($userKey) |
824
|
|
|
{ |
825
|
|
|
$userKey = $this->referenceResolver->resolveReference($userKey); |
826
|
|
|
return $this->userMatcher->matchOneByKey($userKey); |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
/** |
830
|
|
|
* @param int|string $date if integer, we assume a timestamp |
831
|
|
|
* @return \DateTime |
832
|
|
|
*/ |
833
|
|
|
protected function toDateTime($date) |
834
|
|
|
{ |
835
|
|
|
if (is_int($date)) { |
836
|
|
|
return new \DateTime("@" . $date); |
837
|
|
|
} else { |
838
|
|
|
return new \DateTime($date); |
839
|
|
|
} |
840
|
|
|
} |
841
|
|
|
} |
842
|
|
|
|
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.