Skip to content

Commit 5e6e40e

Browse files
authored
IBX-11437: Implemented reference aware external storage (#730)
1 parent e6725b2 commit 5e6e40e

15 files changed

Lines changed: 375 additions & 32 deletions

File tree

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17826,12 +17826,6 @@ parameters:
1782617826
count: 1
1782717827
path: src/lib/Persistence/Legacy/Content/FieldHandler.php
1782817828

17829-
-
17830-
message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldHandler\:\:createExistingFieldInNewVersion\(\) has no return type specified\.$#'
17831-
identifier: missingType.return
17832-
count: 1
17833-
path: src/lib/Persistence/Legacy/Content/FieldHandler.php
17834-
1783517829
-
1783617830
message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Content\\FieldHandler\:\:createNewField\(\) has no return type specified\.$#'
1783717831
identifier: missingType.return
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\Core\FieldType;
10+
11+
use Ibexa\Contracts\Core\Persistence\Content\Field;
12+
use Ibexa\Contracts\Core\Persistence\Content\VersionInfo;
13+
14+
/**
15+
* Interface for external storages that support creating lightweight references
16+
* to another version's data instead of copying it.
17+
*/
18+
interface ReferenceAwareExternalStorage
19+
{
20+
public const REFERENCE_LANGUAGE_CODE = 'reference-language-code';
21+
22+
/**
23+
* Creates a reference to the original field's external data instead of copying it.
24+
*
25+
* Called for fields in languages not being edited during draft creation.
26+
* The implementation should store a lightweight pointer to $originalField's
27+
* external data (identified by `$originalField->versionNo`) and resolve it
28+
* in {@see FieldStorage::getFieldData()}.
29+
*/
30+
public function referenceLegacyField(
31+
VersionInfo $versionInfo,
32+
Field $field,
33+
Field $originalField
34+
): ?bool;
35+
}

src/contracts/Options/Context.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\Core\Options;
10+
11+
use ArrayAccess;
12+
13+
/**
14+
* @implements ArrayAccess<string, mixed>
15+
*/
16+
final class Context implements OptionsBag, ArrayAccess
17+
{
18+
/** @var array<string, mixed> */
19+
private array $data;
20+
21+
/**
22+
* @param array<string, mixed> $data
23+
*/
24+
public function __construct(array $data = [])
25+
{
26+
$this->data = $data;
27+
}
28+
29+
/**
30+
* @return array<string, mixed>
31+
*/
32+
public function all(): array
33+
{
34+
return $this->data;
35+
}
36+
37+
/**
38+
* @param mixed|null $default
39+
*
40+
* @return mixed|null
41+
*/
42+
public function get(string $key, $default = null)
43+
{
44+
if ($this->has($key)) {
45+
return $this->data[$key];
46+
}
47+
48+
return $default;
49+
}
50+
51+
public function has(string $key): bool
52+
{
53+
return array_key_exists($key, $this->data);
54+
}
55+
56+
public function offsetExists($offset): bool
57+
{
58+
return $this->has($offset);
59+
}
60+
61+
/**
62+
* @return mixed
63+
*/
64+
#[\ReturnTypeWillChange]
65+
public function offsetGet($offset)
66+
{
67+
return $this->get($offset);
68+
}
69+
70+
public function offsetSet($offset, $value): void
71+
{
72+
$this->data[$offset] = $value;
73+
}
74+
75+
public function offsetUnset($offset): void
76+
{
77+
unset($this->data[$offset]);
78+
}
79+
}

src/contracts/Persistence/Content/Handler.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Ibexa\Contracts\Core\Persistence\Content;
88

99
// @todo We must verify whether we want to type cast on the "Criterion" interface or abstract class
10+
use Ibexa\Contracts\Core\Options\Context;
1011
use Ibexa\Contracts\Core\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
1112

1213
/**
@@ -41,11 +42,16 @@ public function create(CreateStruct $content);
4142
* @param mixed $contentId
4243
* @param mixed $srcVersion
4344
* @param mixed $userId
44-
* @param string|null $languageCode
4545
*
4646
* @return \Ibexa\Contracts\Core\Persistence\Content
4747
*/
48-
public function createDraftFromVersion($contentId, $srcVersion, $userId, ?string $languageCode = null);
48+
public function createDraftFromVersion(
49+
$contentId,
50+
$srcVersion,
51+
$userId,
52+
?string $languageCode = null,
53+
?Context $context = null
54+
);
4955

5056
/**
5157
* Returns the raw data of a content object identified by $id, in a struct.

src/contracts/Repository/ContentService.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Ibexa\Contracts\Core\Repository;
1010

11+
use Ibexa\Contracts\Core\Options\Context;
1112
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
1213
use Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct;
1314
use Ibexa\Contracts\Core\Repository\Values\Content\ContentDraftList;
@@ -241,7 +242,8 @@ public function createContentDraft(
241242
ContentInfo $contentInfo,
242243
?VersionInfo $versionInfo = null,
243244
?User $creator = null,
244-
?Language $language = null
245+
?Language $language = null,
246+
?Context $context = null
245247
): Content;
246248

247249
/**

src/contracts/Repository/Decorator/ContentServiceDecorator.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Ibexa\Contracts\Core\Repository\Decorator;
1010

11+
use Ibexa\Contracts\Core\Options\Context;
1112
use Ibexa\Contracts\Core\Repository\ContentService;
1213
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
1314
use Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct;
@@ -137,9 +138,10 @@ public function createContentDraft(
137138
ContentInfo $contentInfo,
138139
?VersionInfo $versionInfo = null,
139140
?User $creator = null,
140-
?Language $language = null
141+
?Language $language = null,
142+
?Context $context = null
141143
): Content {
142-
return $this->innerService->createContentDraft($contentInfo, $versionInfo, $creator, $language);
144+
return $this->innerService->createContentDraft($contentInfo, $versionInfo, $creator, $language, $context);
143145
}
144146

145147
public function countContentDrafts(?User $user = null): int

src/lib/Event/ContentService.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Ibexa\Core\Event;
1010

11+
use Ibexa\Contracts\Core\Options\Context;
1112
use Ibexa\Contracts\Core\Repository\ContentService as ContentServiceInterface;
1213
use Ibexa\Contracts\Core\Repository\Decorator\ContentServiceDecorator;
1314
use Ibexa\Contracts\Core\Repository\Events\Content\AddRelationEvent;
@@ -144,7 +145,8 @@ public function createContentDraft(
144145
ContentInfo $contentInfo,
145146
?VersionInfo $versionInfo = null,
146147
?User $creator = null,
147-
?Language $language = null
148+
?Language $language = null,
149+
?Context $context = null
148150
): Content {
149151
$eventData = [
150152
$contentInfo,
@@ -162,7 +164,7 @@ public function createContentDraft(
162164

163165
$contentDraft = $beforeEvent->hasContentDraft()
164166
? $beforeEvent->getContentDraft()
165-
: $this->innerService->createContentDraft($contentInfo, $versionInfo, $creator, $language);
167+
: $this->innerService->createContentDraft($contentInfo, $versionInfo, $creator, $language, $context);
166168

167169
$this->eventDispatcher->dispatch(
168170
new CreateContentDraftEvent($contentDraft, ...$eventData)

src/lib/Persistence/Cache/ContentHandler.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
namespace Ibexa\Core\Persistence\Cache;
88

9+
use Ibexa\Contracts\Core\Options\Context;
910
use Ibexa\Contracts\Core\Persistence\Content;
1011
use Ibexa\Contracts\Core\Persistence\Content\ContentInfo;
1112
use Ibexa\Contracts\Core\Persistence\Content\CreateStruct;
@@ -105,10 +106,21 @@ public function create(CreateStruct $struct)
105106
/**
106107
* {@inheritdoc}
107108
*/
108-
public function createDraftFromVersion($contentId, $srcVersion, $userId, ?string $languageCode = null)
109-
{
109+
public function createDraftFromVersion(
110+
$contentId,
111+
$srcVersion,
112+
$userId,
113+
?string $languageCode = null,
114+
?Context $context = null
115+
) {
110116
$this->logger->logCall(__METHOD__, ['content' => $contentId, 'version' => $srcVersion, 'user' => $userId]);
111-
$draft = $this->persistenceHandler->contentHandler()->createDraftFromVersion($contentId, $srcVersion, $userId, $languageCode);
117+
$draft = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
118+
$contentId,
119+
$srcVersion,
120+
$userId,
121+
$languageCode,
122+
$context
123+
);
112124
$this->cache->deleteItems([
113125
$this->cacheIdentifierGenerator->generateKey(self::CONTENT_VERSION_LIST_IDENTIFIER, [$contentId], true),
114126
]);

src/lib/Persistence/Legacy/Content/FieldHandler.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,16 @@ protected function getEmptyField(FieldDefinition $fieldDefinition, $languageCode
148148
*
149149
* @param \Ibexa\Contracts\Core\Persistence\Content $content
150150
*/
151-
public function createExistingFieldsInNewVersion(Content $content): void
151+
public function createExistingFieldsInNewVersion(Content $content, ?string $editedLanguageCode = null): void
152152
{
153153
foreach ($content->fields as $field) {
154154
if ($field->id === null) {
155155
// Virtual field with default value, skip creating field as it has no id
156156
continue;
157157
}
158-
$this->createExistingFieldInNewVersion($field, $content);
158+
159+
$referenceOnly = $editedLanguageCode !== null && $field->languageCode !== $editedLanguageCode;
160+
$this->createExistingFieldInNewVersion($field, $content, $referenceOnly);
159161
}
160162
}
161163

@@ -270,12 +272,12 @@ protected function updateField(Field $field, Content $content)
270272
* External data is being copied here as some FieldTypes require original field external data.
271273
* By default copying falls back to storing, it is upon external storage implementation to override
272274
* the behaviour as needed.
273-
*
274-
* @param \Ibexa\Contracts\Core\Persistence\Content\Field $field
275-
* @param \Ibexa\Contracts\Core\Persistence\Content $content
276275
*/
277-
protected function createExistingFieldInNewVersion(Field $field, Content $content)
278-
{
276+
protected function createExistingFieldInNewVersion(
277+
Field $field,
278+
Content $content,
279+
bool $referenceOnly = false
280+
): void {
279281
$originalField = clone $field;
280282
$field->versionNo = $content->versionInfo->versionNo;
281283

@@ -288,7 +290,11 @@ protected function createExistingFieldInNewVersion(Field $field, Content $conten
288290
// If the storage handler returns true, it means that $field value has been modified
289291
// So we need to update it in order to store those modifications
290292
// Field converter is called once again via the Mapper
291-
if ($this->storageHandler->copyFieldData($content->versionInfo, $field, $originalField) === true) {
293+
$storageResult = $referenceOnly
294+
? $this->storageHandler->referenceFieldData($content->versionInfo, $field, $originalField)
295+
: $this->storageHandler->copyFieldData($content->versionInfo, $field, $originalField);
296+
297+
if ($storageResult === true) {
292298
$this->contentGateway->updateField(
293299
$field,
294300
$this->mapper->convertToStorageValue($field)

src/lib/Persistence/Legacy/Content/Handler.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
namespace Ibexa\Core\Persistence\Legacy\Content;
88

99
use Exception;
10+
use Ibexa\Contracts\Core\FieldType\ReferenceAwareExternalStorage;
11+
use Ibexa\Contracts\Core\Options\Context;
1012
use Ibexa\Contracts\Core\Persistence\Content;
1113
use Ibexa\Contracts\Core\Persistence\Content\CreateStruct;
1214
use Ibexa\Contracts\Core\Persistence\Content\Handler as BaseContentHandler;
@@ -257,14 +259,18 @@ public function publish($contentId, $versionNo, MetadataUpdateStruct $metaDataUp
257259
* @param mixed $contentId
258260
* @param mixed $srcVersion
259261
* @param mixed $userId
260-
* @param string|null $languageCode
261262
*
262263
* @return \Ibexa\Contracts\Core\Persistence\Content
263264
*
264265
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
265266
*/
266-
public function createDraftFromVersion($contentId, $srcVersion, $userId, ?string $languageCode = null)
267-
{
267+
public function createDraftFromVersion(
268+
$contentId,
269+
$srcVersion,
270+
$userId,
271+
?string $languageCode = null,
272+
?Context $context = null
273+
) {
268274
$content = $this->load($contentId, $srcVersion);
269275

270276
// Create new version
@@ -280,7 +286,10 @@ public function createDraftFromVersion($contentId, $srcVersion, $userId, ?string
280286
);
281287

282288
// Clone fields from previous version and append them to the new one
283-
$this->fieldHandler->createExistingFieldsInNewVersion($content);
289+
$this->fieldHandler->createExistingFieldsInNewVersion(
290+
$content,
291+
$context[ReferenceAwareExternalStorage::REFERENCE_LANGUAGE_CODE] ?? $languageCode
292+
);
284293

285294
// Persist virtual fields
286295
$contentType = $this->contentTypeHandler->load($content->versionInfo->contentInfo->contentTypeId);

0 commit comments

Comments
 (0)