diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5bf5afb553..1465e203cf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -14985,11 +14985,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Content/FieldHandler.php - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\FieldHandler\\:\\:createExistingFieldsInNewVersion\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/FieldHandler.php - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\FieldHandler\\:\\:createNewField\\(\\) has no return type specified\\.$#" count: 1 @@ -16840,26 +16835,6 @@ parameters: count: 2 path: src/lib/Persistence/Legacy/Content/Location/Trash/Handler.php - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractContentFromRows\\(\\) has parameter \\$nameRows with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractContentFromRows\\(\\) has parameter \\$prefix with no type specified\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractContentFromRows\\(\\) has parameter \\$rows with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractContentInfoFromRow\\(\\) has parameter \\$row with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractContentInfoFromRows\\(\\) has parameter \\$rows with no value type specified in iterable type array\\.$#" count: 1 @@ -16900,16 +16875,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Content/Mapper.php - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractVersionInfoListFromRows\\(\\) has parameter \\$nameRows with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - - - message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Mapper\\:\\:extractVersionInfoListFromRows\\(\\) has parameter \\$rows with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/Content/Mapper.php - - message: "#^Property Ibexa\\\\Contracts\\\\Core\\\\Persistence\\\\Content\\\\Field\\:\\:\\$id \\(int\\) does not accept null\\.$#" count: 1 @@ -18740,11 +18705,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Setting/Gateway/ExceptionConversion.php - - - message: "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#" - count: 1 - path: src/lib/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php - - message: "#^Argument of an invalid type array\\\\|iterable supplied for foreach, only iterables are supported\\.$#" count: 1 @@ -29182,7 +29142,7 @@ parameters: - message: "#^Parameter \\#2 \\$fieldDefinition of method Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\ContentTypeService\\:\\:removeFieldDefinition\\(\\) expects Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\ContentType\\\\FieldDefinition, Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\ContentType\\\\FieldDefinition\\|null given\\.$#" - count: 7 + count: 6 path: tests/integration/Core/Repository/ContentTypeServiceTest.php - @@ -48816,7 +48776,7 @@ parameters: path: tests/lib/MVC/Symfony/Templating/Twig/Extension/FileSystemTwigIntegrationTestCase.php - - message: "#^Anonymous function should return string but returns string\\|false\\.$#" + message: "#^Anonymous function should return non\\-empty\\-string but returns non\\-empty\\-string\\|false\\.$#" count: 1 path: tests/lib/MVC/Symfony/Templating/Twig/Extension/RoutingExtensionTest.php @@ -53005,71 +52965,6 @@ parameters: count: 1 path: tests/lib/Persistence/Legacy/Content/Section/SectionHandlerTest.php - - - message: "#^Call to an undefined method Ibexa\\\\Contracts\\\\Core\\\\FieldType\\\\FieldStorage\\:\\:expects\\(\\)\\.$#" - count: 6 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Call to an undefined method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageRegistry\\:\\:expects\\(\\)\\.$#" - count: 4 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:getContextMock\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:getVersionInfoMock\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:testDeleteFieldData\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:testGetFieldDataAvailable\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:testGetFieldDataNotAvailable\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:testStoreFieldData\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Property Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:\\$storageHandler \\(Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandler\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Property Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:\\$storageMock \\(Ibexa\\\\Contracts\\\\Core\\\\FieldType\\\\FieldStorage\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Property Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:\\$storageRegistryMock \\(Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageRegistry\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Property Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:\\$versionInfoMock \\(Ibexa\\\\Core\\\\Repository\\\\Values\\\\Content\\\\VersionInfo\\) does not accept Ibexa\\\\Contracts\\\\Core\\\\Persistence\\\\Content\\\\VersionInfo&PHPUnit\\\\Framework\\\\MockObject\\\\MockObject\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - - - message: "#^Property Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageHandlerTest\\:\\:\\$versionInfoMock \\(Ibexa\\\\Core\\\\Repository\\\\Values\\\\Content\\\\VersionInfo\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\StorageRegistryTest\\:\\:testGetNotFound\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php b/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php new file mode 100644 index 0000000000..df579341c1 --- /dev/null +++ b/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php @@ -0,0 +1,77 @@ + */ + private array $context; + + private ?Field $field; + + /** + * @param array $context + */ + public function __construct( + Content $content, + FieldDefinition $fieldDefinition, + string $languageCode, + array $context = [] + ) { + $this->content = $content; + $this->fieldDefinition = $fieldDefinition; + $this->languageCode = $languageCode; + $this->context = $context; + $this->field = null; + } + + public function getContent(): Content + { + return $this->content; + } + + public function getFieldDefinition(): FieldDefinition + { + return $this->fieldDefinition; + } + + public function getLanguageCode(): string + { + return $this->languageCode; + } + + /** + * @return array + */ + public function getContext(): array + { + return $this->context; + } + + public function setField(?Field $field): void + { + $this->field = $field; + } + + public function getField(): ?Field + { + return $this->field; + } +} diff --git a/src/contracts/FieldType/DefaultDataFieldStorage.php b/src/contracts/FieldType/DefaultDataFieldStorage.php new file mode 100644 index 0000000000..6834473d2e --- /dev/null +++ b/src/contracts/FieldType/DefaultDataFieldStorage.php @@ -0,0 +1,24 @@ +$field value property with default data based on the external data. + * + * $field->value is a {@see \Ibexa\Contracts\Core\Persistence\Content\FieldValue} object. + * This value holds the data as a {@see \Ibexa\Core\FieldType\Value} based object, according to + * the field type (e.g. for TextLine, it will be a {@see \Ibexa\Core\FieldType\TextLine\Value} object). + */ + public function getDefaultFieldData(VersionInfo $versionInfo, Field $field): void; +} diff --git a/src/contracts/Repository/Values/Content/Field.php b/src/contracts/Repository/Values/Content/Field.php index 6114c1c479..627502d6a5 100644 --- a/src/contracts/Repository/Values/Content/Field.php +++ b/src/contracts/Repository/Values/Content/Field.php @@ -24,7 +24,8 @@ class Field extends ValueObject /** * The field id. * - * @todo may be not needed + * Value of `null` indicates the field is virtual + * and is not persisted (yet). * * @var mixed */ @@ -58,7 +59,7 @@ class Field extends ValueObject */ protected $fieldTypeIdentifier; - public function getId(): int + public function getId(): ?int { return $this->id; } @@ -85,6 +86,14 @@ public function getFieldTypeIdentifier(): string { return $this->fieldTypeIdentifier; } + + /** + * @phpstan-assert-if-true !null $this->getId() + */ + public function isVirtual(): bool + { + return null === $this->id; + } } class_alias(Field::class, 'eZ\Publish\API\Repository\Values\Content\Field'); diff --git a/src/lib/Persistence/Legacy/Content/FieldHandler.php b/src/lib/Persistence/Legacy/Content/FieldHandler.php index da16face3b..36f72d3d82 100644 --- a/src/lib/Persistence/Legacy/Content/FieldHandler.php +++ b/src/lib/Persistence/Legacy/Content/FieldHandler.php @@ -148,9 +148,13 @@ protected function getEmptyField(FieldDefinition $fieldDefinition, $languageCode * * @param \Ibexa\Contracts\Core\Persistence\Content $content */ - public function createExistingFieldsInNewVersion(Content $content) + public function createExistingFieldsInNewVersion(Content $content): void { foreach ($content->fields as $field) { + if ($field->id === null) { + // Virtual field with default value, skip creating field as it has no id + continue; + } $this->createExistingFieldInNewVersion($field, $content); } } diff --git a/src/lib/Persistence/Legacy/Content/Mapper.php b/src/lib/Persistence/Legacy/Content/Mapper.php index b7cf7823be..3113470b28 100644 --- a/src/lib/Persistence/Legacy/Content/Mapper.php +++ b/src/lib/Persistence/Legacy/Content/Mapper.php @@ -6,6 +6,7 @@ */ namespace Ibexa\Core\Persistence\Legacy\Content; +use Ibexa\Contracts\Core\Event\Mapper\ResolveMissingFieldEvent; use Ibexa\Contracts\Core\Persistence\Content; use Ibexa\Contracts\Core\Persistence\Content\ContentInfo; use Ibexa\Contracts\Core\Persistence\Content\CreateStruct; @@ -14,41 +15,66 @@ use Ibexa\Contracts\Core\Persistence\Content\Language\Handler as LanguageHandler; use Ibexa\Contracts\Core\Persistence\Content\Relation; use Ibexa\Contracts\Core\Persistence\Content\Relation\CreateStruct as RelationCreateStruct; +use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; use Ibexa\Core\Base\Exceptions\NotFoundException; use Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry as Registry; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Mapper for Content Handler. * * Performs mapping of Content objects. + * + * @phpstan-type TVersionedLanguageFieldDefinitionsMap array< + * int, array< + * int, array< + * string, array< + * int, \Ibexa\Contracts\Core\Persistence\Content\Type\FieldDefinition, + * > + * > + * > + * > + * @phpstan-type TVersionedFieldMap array< + * int, array< + * int, array< + * int, \Ibexa\Contracts\Core\Persistence\Content\Field, + * > + * > + * > + * @phpstan-type TVersionedNameMap array< + * int, array< + * int, array + * > + * > + * @phpstan-type TContentInfoMap array + * @phpstan-type TVersionInfoMap array< + * int, array< + * int, \Ibexa\Contracts\Core\Persistence\Content\VersionInfo, + * > + * > + * @phpstan-type TRawContentRow array */ class Mapper { - /** - * FieldValue converter registry. - * - * @var \Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry - */ - protected $converterRegistry; + protected Registry $converterRegistry; - /** - * Caching language handler. - * - * @var \Ibexa\Contracts\Core\Persistence\Content\Language\Handler - */ - protected $languageHandler; + protected LanguageHandler $languageHandler; - /** - * Creates a new mapper. - * - * @param \Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry $converterRegistry - * @param \Ibexa\Contracts\Core\Persistence\Content\Language\Handler $languageHandler - */ - public function __construct(Registry $converterRegistry, LanguageHandler $languageHandler) - { + private ContentTypeHandler $contentTypeHandler; + + private EventDispatcherInterface $eventDispatcher; + + public function __construct( + Registry $converterRegistry, + LanguageHandler $languageHandler, + ContentTypeHandler $contentTypeHandler, + EventDispatcherInterface $eventDispatcher + ) { $this->converterRegistry = $converterRegistry; $this->languageHandler = $languageHandler; + $this->contentTypeHandler = $contentTypeHandler; + $this->eventDispatcher = $eventDispatcher; } /** @@ -174,59 +200,120 @@ public function convertToStorageValue(Field $field) * * "$tableName_$columnName" * - * @param array $rows - * @param array $nameRows + * @param array> $rows + * @param array> $nameRows + * @param string $prefix * * @return \Ibexa\Contracts\Core\Persistence\Content[] */ - public function extractContentFromRows(array $rows, array $nameRows, $prefix = 'ezcontentobject_') - { + public function extractContentFromRows( + array $rows, + array $nameRows, + string $prefix = 'ezcontentobject_' + ): array { $versionedNameData = []; + foreach ($nameRows as $row) { - $contentId = (int)$row['ezcontentobject_name_contentobject_id']; - $versionNo = (int)$row['ezcontentobject_name_content_version']; - $versionedNameData[$contentId][$versionNo][$row['ezcontentobject_name_content_translation']] = $row['ezcontentobject_name_name']; + $contentId = (int)$row["{$prefix}name_contentobject_id"]; + $versionNo = (int)$row["{$prefix}name_content_version"]; + $languageCode = (string)$row["{$prefix}name_content_translation"]; + $versionedNameData[$contentId][$versionNo][$languageCode] = (string)$row["{$prefix}name_name"]; } $contentInfos = []; $versionInfos = []; $fields = []; + $fieldDefinitions = $this->loadCachedVersionFieldDefinitionsPerLanguage( + $rows, + $prefix + ); + foreach ($rows as $row) { $contentId = (int)$row["{$prefix}id"]; + $versionId = (int)$row["{$prefix}version_id"]; + if (!isset($contentInfos[$contentId])) { $contentInfos[$contentId] = $this->extractContentInfoFromRow($row, $prefix); } + if (!isset($versionInfos[$contentId])) { $versionInfos[$contentId] = []; } - $versionId = (int)$row['ezcontentobject_version_id']; if (!isset($versionInfos[$contentId][$versionId])) { $versionInfos[$contentId][$versionId] = $this->extractVersionInfoFromRow($row); } - $fieldId = (int)$row['ezcontentobject_attribute_id']; - if (!isset($fields[$contentId][$versionId][$fieldId])) { + $fieldId = (int)$row["{$prefix}attribute_id"]; + $fieldDefinitionId = (int)$row["{$prefix}attribute_contentclassattribute_id"]; + $languageCode = $row["{$prefix}attribute_language_code"]; + + if (!isset($fields[$contentId][$versionId][$fieldId]) + && isset($fieldDefinitions[$contentId][$versionId][$languageCode][$fieldDefinitionId]) + ) { $fields[$contentId][$versionId][$fieldId] = $this->extractFieldFromRow($row); + unset($fieldDefinitions[$contentId][$versionId][$languageCode][$fieldDefinitionId]); } } + return $this->buildContentObjects( + $contentInfos, + $versionInfos, + $fields, + $fieldDefinitions, + $versionedNameData + ); + } + + /** + * @phpstan-param TContentInfoMap $contentInfos + * @phpstan-param TVersionInfoMap $versionInfos + * @phpstan-param TVersionedFieldMap $fields + * @phpstan-param TVersionedLanguageFieldDefinitionsMap $missingFieldDefinitions + * @phpstan-param TVersionedNameMap $versionedNames + * + * @return \Ibexa\Contracts\Core\Persistence\Content[] + */ + private function buildContentObjects( + array $contentInfos, + array $versionInfos, + array $fields, + array $missingFieldDefinitions, + array $versionedNames + ): array { $results = []; + foreach ($contentInfos as $contentId => $contentInfo) { foreach ($versionInfos[$contentId] as $versionId => $versionInfo) { // Fallback to just main language name if versioned name data is missing - if (isset($versionedNameData[$contentId][$versionInfo->versionNo])) { - $names = $versionedNameData[$contentId][$versionInfo->versionNo]; - } else { - $names = [$contentInfo->mainLanguageCode => $contentInfo->name]; - } + $names = $versionedNames[$contentId][$versionInfo->versionNo] + ?? [$contentInfo->mainLanguageCode => $contentInfo->name]; $content = new Content(); $content->versionInfo = $versionInfo; $content->versionInfo->names = $names; $content->versionInfo->contentInfo = $contentInfo; $content->fields = array_values($fields[$contentId][$versionId]); + + $missingVersionFieldDefinitions = $missingFieldDefinitions[$contentId][$versionId]; + foreach ($missingVersionFieldDefinitions as $languageCode => $versionFieldDefinitions) { + foreach ($versionFieldDefinitions as $fieldDefinition) { + $event = $this->eventDispatcher->dispatch( + new ResolveMissingFieldEvent( + $content, + $fieldDefinition, + $languageCode + ) + ); + + $field = $event->getField(); + if ($field !== null) { + $content->fields[] = $field; + } + } + } + $results[] = $content; } } @@ -234,10 +321,50 @@ public function extractContentFromRows(array $rows, array $nameRows, $prefix = ' return $results; } + /** + * @phpstan-param TRawContentRow[] $rows + * + * @phpstan-return TVersionedLanguageFieldDefinitionsMap + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function loadCachedVersionFieldDefinitionsPerLanguage( + array $rows, + string $prefix + ): array { + $fieldDefinitions = []; + $contentTypes = []; + $allLanguages = $this->loadAllLanguagesWithIdKey(); + + foreach ($rows as $row) { + $contentId = (int)$row["{$prefix}id"]; + $versionId = (int)$row["{$prefix}version_id"]; + $contentTypeId = (int)$row["{$prefix}contentclass_id"]; + $languageMask = (int)$row["{$prefix}version_language_mask"]; + + if (isset($fieldDefinitions[$contentId][$versionId])) { + continue; + } + + $languageCodes = $this->extractLanguageCodesFromMask($languageMask, $allLanguages); + $contentTypes[$contentTypeId] = $contentTypes[$contentTypeId] ?? $this->contentTypeHandler->load($contentTypeId); + $contentType = $contentTypes[$contentTypeId]; + foreach ($contentType->fieldDefinitions as $fieldDefinition) { + foreach ($languageCodes as $languageCode) { + $id = (int)$fieldDefinition->id; + $fieldDefinitions[$contentId][$versionId][$languageCode][$id] = $fieldDefinition; + } + } + } + + return $fieldDefinitions; + } + /** * Extracts a ContentInfo object from $row. * - * @param array $row + * @phpstan-param TRawContentRow $row + * * @param string $prefix Prefix for row keys, which are initially mapped by ezcontentobject fields * @param string $treePrefix Prefix for tree row key, which are initially mapped by ezcontentobject_tree_ fields * @@ -247,17 +374,16 @@ public function extractContentInfoFromRow(array $row, $prefix = '', $treePrefix { $contentInfo = new ContentInfo(); $contentInfo->id = (int)$row["{$prefix}id"]; - $contentInfo->name = $row["{$prefix}name"]; + $contentInfo->name = (string)$row["{$prefix}name"]; $contentInfo->contentTypeId = (int)$row["{$prefix}contentclass_id"]; $contentInfo->sectionId = (int)$row["{$prefix}section_id"]; $contentInfo->currentVersionNo = (int)$row["{$prefix}current_version"]; - $contentInfo->isPublished = ($row["{$prefix}status"] == ContentInfo::STATUS_PUBLISHED); $contentInfo->ownerId = (int)$row["{$prefix}owner_id"]; $contentInfo->publicationDate = (int)$row["{$prefix}published"]; $contentInfo->modificationDate = (int)$row["{$prefix}modified"]; - $contentInfo->alwaysAvailable = 1 === ($row["{$prefix}language_mask"] & 1); + $contentInfo->alwaysAvailable = 1 === ((int)$row["{$prefix}language_mask"] & 1); $contentInfo->mainLanguageCode = $this->languageHandler->load($row["{$prefix}initial_language_id"])->languageCode; - $contentInfo->remoteId = $row["{$prefix}remote_id"]; + $contentInfo->remoteId = (string)$row["{$prefix}remote_id"]; $contentInfo->mainLocationId = ($row["{$treePrefix}main_node_id"] !== null ? (int)$row["{$treePrefix}main_node_id"] : null); $contentInfo->status = (int)$row["{$prefix}status"]; $contentInfo->isPublished = ($contentInfo->status == ContentInfo::STATUS_PUBLISHED); @@ -335,8 +461,8 @@ private function extractVersionInfoFromRow(array $row, array $names = []) /** * Extracts a VersionInfo object from $row. * - * @param array $rows - * @param array $nameRows + * @phpstan-param TRawContentRow[] $rows + * @phpstan-param TRawContentRow[] $nameRows * * @return \Ibexa\Contracts\Core\Persistence\Content\VersionInfo[] */ diff --git a/src/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriber.php b/src/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriber.php new file mode 100644 index 0000000000..36903a50f3 --- /dev/null +++ b/src/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriber.php @@ -0,0 +1,222 @@ +converterRegistry = $converterRegistry; + $this->storageRegistry = $storageRegistry; + $this->contentGateway = $contentGateway; + } + + public static function getSubscribedEvents(): array + { + return [ + ResolveMissingFieldEvent::class => [ + ['persistExternalStorageField', -100], + ['resolveVirtualExternalStorageField', -80], + ['resolveVirtualField', 0], + ], + ]; + } + + public function resolveVirtualField(ResolveMissingFieldEvent $event): void + { + if ($event->getField()) { + return; + } + + $content = $event->getContent(); + + try { + $emptyField = $this->createEmptyField( + $content->versionInfo, + $event->getFieldDefinition(), + $event->getLanguageCode() + ); + + $event->setField($emptyField); + } catch (NotFound $exception) { + return; + } + } + + /** + * @throws \Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter\Exception\NotFound + */ + public function persistExternalStorageField(ResolveMissingFieldEvent $event): void + { + $field = $event->getField(); + + if ($field === null) { + // Nothing to persist + return; + } + + if ($field->id !== null) { + // Not a virtual field + return; + } + + $fieldDefinition = $event->getFieldDefinition(); + $storage = $this->storageRegistry->getStorage($fieldDefinition->fieldType); + + if ($storage instanceof NullStorage) { + // Not an external storage + return; + } + + $content = $event->getContent(); + + $field->id = $this->contentGateway->insertNewField( + $content, + $field, + $this->getDefaultStorageValue() + ); + + if ($field->value->data !== null) { + $result = $storage->storeFieldData( + $content->versionInfo, + $field, + [] + ); + + if ($result === true) { + $storageValue = new StorageFieldValue(); + $converter = $this->converterRegistry->getConverter($fieldDefinition->fieldType); + $converter->toStorageValue( + $field->value, + $storageValue + ); + + $this->contentGateway->updateField( + $field, + $storageValue + ); + } + } + + $storage->getFieldData( + $content->versionInfo, + $field, + [] + ); + + $event->setField($field); + } + + public function resolveVirtualExternalStorageField(ResolveMissingFieldEvent $event): void + { + $field = $event->getField(); + + if ($field === null) { + // Nothing to resolve + return; + } + + if ($field->id !== null) { + // Not a virtual field + return; + } + + $fieldDefinition = $event->getFieldDefinition(); + $storage = $this->storageRegistry->getStorage($fieldDefinition->fieldType); + + if ($storage instanceof NullStorage) { + // Not an external storage + return; + } + + if (!$storage instanceof DefaultDataFieldStorage) { + return; + } + + $content = $event->getContent(); + + $storage->getDefaultFieldData( + $content->versionInfo, + $field + ); + + $event->setField($field); + + // Do not persist the external storage field + $event->stopPropagation(); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function createEmptyField( + VersionInfo $versionInfo, + FieldDefinition $fieldDefinition, + string $languageCode + ): Field { + $field = new Field(); + $field->fieldDefinitionId = $fieldDefinition->id; + $field->type = $fieldDefinition->fieldType; + $field->value = $this->getDefaultValue($fieldDefinition); + $field->languageCode = $languageCode; + $field->versionNo = $versionInfo->versionNo; + + return $field; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function getDefaultValue(FieldDefinition $fieldDefinition): FieldValue + { + $value = clone $fieldDefinition->defaultValue; + $storageValue = $this->getDefaultStorageValue(); + + $converter = $this->converterRegistry->getConverter($fieldDefinition->fieldType); + $converter->toStorageValue($value, $storageValue); + $converter->toFieldValue($storageValue, $value); + + return $value; + } + + private function getDefaultStorageValue(): StorageFieldValue + { + $storageValue = new StorageFieldValue(); + $storageValue->dataFloat = 0; + $storageValue->dataInt = 0; + $storageValue->dataText = ''; + $storageValue->sortKeyInt = 0; + $storageValue->sortKeyString = ''; + + return $storageValue; + } +} diff --git a/src/lib/Persistence/Legacy/Content/StorageHandler.php b/src/lib/Persistence/Legacy/Content/StorageHandler.php index 475f6a1e98..b4e9ded93d 100644 --- a/src/lib/Persistence/Legacy/Content/StorageHandler.php +++ b/src/lib/Persistence/Legacy/Content/StorageHandler.php @@ -79,7 +79,7 @@ public function copyFieldData(VersionInfo $versionInfo, Field $field, Field $ori public function getFieldData(VersionInfo $versionInfo, Field $field) { $storage = $this->storageRegistry->getStorage($field->type); - if ($storage->hasFieldData()) { + if ($field->id !== null && $storage->hasFieldData()) { $storage->getFieldData($versionInfo, $field, $this->context); } } diff --git a/src/lib/Persistence/Legacy/Content/Type/Handler.php b/src/lib/Persistence/Legacy/Content/Type/Handler.php index af6b85e3b3..88abfb59ef 100644 --- a/src/lib/Persistence/Legacy/Content/Type/Handler.php +++ b/src/lib/Persistence/Legacy/Content/Type/Handler.php @@ -632,7 +632,6 @@ public function publish($contentTypeId) try { $fromType = $this->load($contentTypeId, Type::STATUS_DEFINED); - $this->updateHandler->updateContentObjects($fromType, $toType); $this->updateHandler->deleteOldType($fromType); } catch (Exception\TypeNotFound $e) { // If no old type is found, no updates are necessary to it diff --git a/src/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php b/src/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php index 15d3d38558..d27416712d 100644 --- a/src/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php +++ b/src/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php @@ -9,7 +9,6 @@ namespace Ibexa\Core\Persistence\Legacy\Content\Type\Update\Handler; use Ibexa\Contracts\Core\Persistence\Content\Type; -use Ibexa\Core\Persistence\Legacy\Content\Type\ContentUpdater; use Ibexa\Core\Persistence\Legacy\Content\Type\Gateway; use Ibexa\Core\Persistence\Legacy\Content\Type\Update\Handler; @@ -23,21 +22,14 @@ final class DoctrineDatabase extends Handler /** @var \Ibexa\Core\Persistence\Legacy\Content\Type\Gateway */ protected $contentTypeGateway; - /** @var \Ibexa\Core\Persistence\Legacy\Content\Type\ContentUpdater */ - protected $contentUpdater; - - public function __construct(Gateway $contentTypeGateway, ContentUpdater $contentUpdater) + public function __construct(Gateway $contentTypeGateway) { $this->contentTypeGateway = $contentTypeGateway; - $this->contentUpdater = $contentUpdater; } public function updateContentObjects(Type $fromType, Type $toType): void { - $this->contentUpdater->applyUpdates( - $fromType->id, - $this->contentUpdater->determineActions($fromType, $toType) - ); + // Do nothing, content objects are no longer updated } public function deleteOldType(Type $fromType): void diff --git a/src/lib/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php b/src/lib/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php index 7b87d22156..31ed4d15bd 100644 --- a/src/lib/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php +++ b/src/lib/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php @@ -8,8 +8,6 @@ namespace Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\FetchMode; use Ibexa\Core\Base\Exceptions\DatabaseException; use Ibexa\Core\Persistence\Legacy\SharedGateway\Gateway; @@ -20,39 +18,19 @@ final class SqliteGateway implements Gateway */ private const FATAL_ERROR_CODE = 7; - /** @var \Doctrine\DBAL\Connection */ - private $connection; - - /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */ - private $databasePlatform; - - /** @var int[] */ + /** @var array */ private $lastInsertedIds = []; - /** - * @throws \Doctrine\DBAL\DBALException - */ - public function __construct(Connection $connection) - { - $this->connection = $connection; - $this->databasePlatform = $connection->getDatabasePlatform(); - } - public function getColumnNextIntegerValue( string $tableName, string $columnName, string $sequenceName ): ?int { - $query = $this->connection->createQueryBuilder(); - $query - ->select($this->databasePlatform->getMaxExpression($columnName)) - ->from($tableName); - - $lastId = (int)$query->execute()->fetch(FetchMode::COLUMN); + $lastId = $this->lastInsertedIds[$sequenceName] ?? 0; + $nextId = (int)hrtime(true); - $this->lastInsertedIds[$sequenceName] = $lastId + 1; - - return $this->lastInsertedIds[$sequenceName]; + // $lastId === $nextId shouldn't happen using high-resolution time, but better safe than sorry + return $this->lastInsertedIds[$sequenceName] = $lastId === $nextId ? $nextId + 1 : $nextId; } /** diff --git a/src/lib/Resources/settings/storage_engines/legacy/content.yml b/src/lib/Resources/settings/storage_engines/legacy/content.yml index df3457e00e..713346f146 100644 --- a/src/lib/Resources/settings/storage_engines/legacy/content.yml +++ b/src/lib/Resources/settings/storage_engines/legacy/content.yml @@ -9,6 +9,16 @@ services: arguments: - '@Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry' - '@ibexa.spi.persistence.legacy.language.handler' + - '@ibexa.spi.persistence.legacy.content_type.handler' + - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' + + Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber: + arguments: + $converterRegistry: '@Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry' + $storageRegistry: '@Ibexa\Core\Persistence\Legacy\Content\StorageRegistry' + $contentGateway: '@ibexa.persistence.legacy.content.gateway' + tags: + - { name: kernel.event_subscriber } Ibexa\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase.inner: class: Ibexa\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase diff --git a/src/lib/Resources/settings/storage_engines/legacy/content_type.yml b/src/lib/Resources/settings/storage_engines/legacy/content_type.yml index 06b1059ae8..a4c2d40a77 100644 --- a/src/lib/Resources/settings/storage_engines/legacy/content_type.yml +++ b/src/lib/Resources/settings/storage_engines/legacy/content_type.yml @@ -39,7 +39,6 @@ services: class: Ibexa\Core\Persistence\Legacy\Content\Type\Update\Handler\DoctrineDatabase arguments: - '@ibexa.persistence.legacy.content_type.gateway' - - '@Ibexa\Core\Persistence\Legacy\Content\Type\ContentUpdater' ibexa.persistence.legacy.content_type.update_handler: alias: Ibexa\Core\Persistence\Legacy\Content\Type\Update\Handler\DoctrineDatabase diff --git a/tests/integration/Core/Repository/ContentTypeServiceTest.php b/tests/integration/Core/Repository/ContentTypeServiceTest.php index a503bb4bfd..d5b47f6b9a 100644 --- a/tests/integration/Core/Repository/ContentTypeServiceTest.php +++ b/tests/integration/Core/Repository/ContentTypeServiceTest.php @@ -2085,66 +2085,6 @@ public function testRemoveFieldDefinitionRemovesFieldFromContentRemoved($data) ); } - /** - * @covers \Ibexa\Contracts\Core\Repository\ContentTypeService::removeFieldDefinition() - */ - public function testRemoveFieldDefinitionRemovesOrphanedRelations(): void - { - $repository = $this->getRepository(); - - $contentTypeService = $repository->getContentTypeService(); - $contentService = $repository->getContentService(); - - // Create ContentType - $contentTypeDraft = $this->createContentTypeDraft([$this->getRelationFieldDefinition()]); - $contentTypeService->publishContentTypeDraft($contentTypeDraft); - $publishedType = $contentTypeService->loadContentType($contentTypeDraft->id); - - // Create Content with Relation - $contentDraft = $this->createContentDraft(); - $publishedVersion = $contentService->publishVersion($contentDraft->versionInfo); - - $newDraft = $contentService->createContentDraft($publishedVersion->contentInfo); - $updateStruct = $contentService->newContentUpdateStruct(); - $updateStruct->setField('relation', 14, 'eng-US'); - $contentDraft = $contentService->updateContent($newDraft->versionInfo, $updateStruct); - $publishedContent = $contentService->publishVersion($contentDraft->versionInfo); - - // Remove field definition from ContentType - $contentTypeDraft = $contentTypeService->createContentTypeDraft($publishedType); - $relationField = $contentTypeDraft->getFieldDefinition('relation'); - $contentTypeService->removeFieldDefinition($contentTypeDraft, $relationField); - $contentTypeService->publishContentTypeDraft($contentTypeDraft); - - // Load Content - $content = $contentService->loadContent($publishedContent->contentInfo->id); - - $this->assertCount(0, $contentService->loadRelations($content->versionInfo)); - } - - private function getRelationFieldDefinition(): FieldDefinitionCreateStruct - { - $repository = $this->getRepository(); - - $contentTypeService = $repository->getContentTypeService(); - - $relationFieldCreate = $contentTypeService->newFieldDefinitionCreateStruct( - 'relation', - 'ezobjectrelation' - ); - $relationFieldCreate->names = ['eng-US' => 'Relation']; - $relationFieldCreate->descriptions = ['eng-US' => 'Relation to any Content']; - $relationFieldCreate->fieldGroup = 'blog-content'; - $relationFieldCreate->position = 3; - $relationFieldCreate->isTranslatable = false; - $relationFieldCreate->isRequired = false; - $relationFieldCreate->isInfoCollector = false; - $relationFieldCreate->validatorConfiguration = []; - $relationFieldCreate->isSearchable = false; - - return $relationFieldCreate; - } - /** * Test for the addFieldDefinition() method. * diff --git a/tests/integration/Core/Resources/settings/common.yml b/tests/integration/Core/Resources/settings/common.yml index d2aad48c13..2bedb7dd96 100644 --- a/tests/integration/Core/Resources/settings/common.yml +++ b/tests/integration/Core/Resources/settings/common.yml @@ -21,6 +21,7 @@ services: - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\TrashEventSubscriber' ] ] - [ 'addSubscriber', [ '@Ibexa\Core\Search\Common\EventSubscriber\UserEventSubscriber' ] ] - [ 'addSubscriber', [ '@Ibexa\Core\Repository\EventSubscriber\NameSchemaSubscriber' ] ] + - [ 'addSubscriber', [ '@Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber' ] ] Symfony\Contracts\EventDispatcher\EventDispatcherInterface: '@Symfony\Component\EventDispatcher\EventDispatcher' diff --git a/tests/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriberTest.php b/tests/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriberTest.php new file mode 100644 index 0000000000..38beb6aaa7 --- /dev/null +++ b/tests/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriberTest.php @@ -0,0 +1,336 @@ +getConverterRegistry(); + + $contentGateway = $this->createMock(ContentGateway::class); + $contentGateway->expects($this->never())->method('insertNewField'); + + $storageRegistry = $this->createMock(StorageRegistry::class); + $storageRegistry->method('getStorage')->willReturn(new NullStorage()); + + $eventDispatcher = $this->getEventDispatcher( + $converterRegistry, + $storageRegistry, + $contentGateway + ); + + $event = $eventDispatcher->dispatch( + $this->getEvent([ + 'id' => 123, + 'identifier' => 'example_field', + 'fieldType' => 'some_type', + 'defaultValue' => new Content\FieldValue(), + ]) + ); + + $expected = new Content\Field([ + 'id' => null, + 'fieldDefinitionId' => 123, + 'type' => 'some_type', + 'value' => new Content\FieldValue(), + 'languageCode' => 'eng-GB', + 'versionNo' => 123, + ]); + + self::assertEquals( + $expected, + $event->getField() + ); + + self::assertCount(3, $eventDispatcher->getCalledListeners()); + self::assertEquals( + [ + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualExternalStorageField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::persistExternalStorageField', + ], + array_column($eventDispatcher->getCalledListeners(), 'pretty') + ); + } + + public function testResolveVirtualExternalStorageField(): void + { + $converterRegistry = $this->getConverterRegistry(); + + $contentGateway = $this->createMock(ContentGateway::class); + $contentGateway->expects($this->never())->method('insertNewField'); + + $defaultFieldStorageMock = $this->createMock(DefaultDataFieldStorage::class); + $defaultFieldStorageMock + ->method('getDefaultFieldData') + ->willReturnCallback( + static function (VersionInfo $versionInfo, Field $field): void { + $field->value->externalData = [ + 'some_default' => 'external_data', + ]; + } + ); + $storageRegistry = $this->createMock(StorageRegistry::class); + $storageRegistry->method('getStorage') + ->willReturn($defaultFieldStorageMock); + + $eventDispatcher = $this->getEventDispatcher( + $converterRegistry, + $storageRegistry, + $contentGateway + ); + + $event = $eventDispatcher->dispatch( + $this->getEvent([ + 'id' => 678, + 'identifier' => 'example_external_field', + 'fieldType' => 'external_type_virtual', + 'defaultValue' => new Content\FieldValue(), + ]) + ); + + $expected = new Content\Field([ + 'id' => null, + 'fieldDefinitionId' => 678, + 'type' => 'external_type_virtual', + 'value' => new Content\FieldValue([ + 'externalData' => [ + 'some_default' => 'external_data', + ], + ]), + 'languageCode' => 'eng-GB', + 'versionNo' => 123, + ]); + + self::assertEquals( + $expected, + $event->getField() + ); + + self::assertCount(1, $eventDispatcher->getNotCalledListeners()); + self::assertEquals( + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::persistExternalStorageField', + $eventDispatcher->getNotCalledListeners()[0]['pretty'] + ); + } + + public function testPersistEmptyExternalStorageField(): void + { + $converterRegistry = $this->getConverterRegistry(); + + $storage = $this->createMock(FieldStorage::class); + $storage->expects($this->never())->method('storeFieldData'); + + $storage->expects($this->once()) + ->method('getFieldData') + ->willReturnCallback(static function (VersionInfo $versionInfo, Field $field) { + $field->value->externalData = [ + 'some_default' => 'external_data', + ]; + }); + + $storageRegistry = $this->createMock(StorageRegistry::class); + $storageRegistry->method('getStorage')->willReturn($storage); + + $contentGateway = $this->createMock(ContentGateway::class); + $contentGateway->expects($this->once())->method('insertNewField') + ->willReturn(567); + + $eventDispatcher = $this->getEventDispatcher( + $converterRegistry, + $storageRegistry, + $contentGateway + ); + + $event = $eventDispatcher->dispatch( + $this->getEvent([ + 'id' => 123, + 'identifier' => 'example_field', + 'fieldType' => 'external_type', + 'defaultValue' => new Content\FieldValue(), + ]) + ); + + $expected = new Content\Field([ + 'id' => 567, + 'fieldDefinitionId' => 123, + 'type' => 'external_type', + 'value' => new Content\FieldValue([ + 'externalData' => [ + 'some_default' => 'external_data', + ], + ]), + 'languageCode' => 'eng-GB', + 'versionNo' => 123, + ]); + + self::assertEquals( + $expected, + $event->getField() + ); + + self::assertCount(3, $eventDispatcher->getCalledListeners()); + self::assertEquals( + [ + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualExternalStorageField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::persistExternalStorageField', + ], + array_column($eventDispatcher->getCalledListeners(), 'pretty') + ); + } + + public function testPersistExternalStorageField(): void + { + $converterRegistry = $this->getConverterRegistry(); + + $storage = $this->createMock(FieldStorage::class); + $storage->expects($this->once()) + ->method('storeFieldData') + ->willReturnCallback(static function (VersionInfo $versionInfo, Field $field) { + $field->value->externalData = $field->value->data; + }); + + $storage->expects($this->once())->method('getFieldData'); + + $storageRegistry = $this->createMock(StorageRegistry::class); + $storageRegistry->method('getStorage')->willReturn($storage); + + $contentGateway = $this->createMock(ContentGateway::class); + $contentGateway->expects($this->once())->method('insertNewField') + ->willReturn(456); + + $eventDispatcher = $this->getEventDispatcher( + $converterRegistry, + $storageRegistry, + $contentGateway + ); + + $event = $eventDispatcher->dispatch( + $this->getEvent([ + 'id' => 123, + 'identifier' => 'example_field', + 'fieldType' => 'external_type', + 'defaultValue' => new Content\FieldValue([ + 'data' => ['some_data' => 'to_be_stored'], + ]), + ]) + ); + + $expected = new Content\Field([ + 'id' => 456, + 'fieldDefinitionId' => 123, + 'type' => 'external_type', + 'value' => new Content\FieldValue([ + 'data' => [ + 'some_data' => 'to_be_stored', + ], + 'externalData' => [ + 'some_data' => 'to_be_stored', + ], + ]), + 'languageCode' => 'eng-GB', + 'versionNo' => 123, + ]); + + self::assertEquals( + $expected, + $event->getField() + ); + + self::assertCount(3, $eventDispatcher->getCalledListeners()); + self::assertEquals( + [ + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::resolveVirtualExternalStorageField', + 'Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber::persistExternalStorageField', + ], + array_column($eventDispatcher->getCalledListeners(), 'pretty') + ); + } + + private function getContent(): Content + { + $versionInfo = $this->getVersionInfo(); + + $content = new Content(); + $content->versionInfo = $versionInfo; + $content->fields = []; + + return $content; + } + + private function getVersionInfo(): VersionInfo + { + $versionInfo = new VersionInfo(); + $versionInfo->versionNo = 123; + + return $versionInfo; + } + + private function getEventDispatcher( + ConverterRegistry $converterRegistry, + StorageRegistry $storageRegistry, + ContentGateway $contentGateway + ): TraceableEventDispatcher { + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber( + new ResolveVirtualFieldSubscriber( + $converterRegistry, + $storageRegistry, + $contentGateway, + ) + ); + + return new TraceableEventDispatcher( + $eventDispatcher, + new Stopwatch() + ); + } + + private function getConverterRegistry(): ConverterRegistry + { + $converterRegistry = $this->createMock(ConverterRegistry::class); + $converterRegistry->method('getConverter') + ->willReturn($this->createMock(Converter::class)); + + return $converterRegistry; + } + + /** + * @param array $fieldDefinition + */ + private function getEvent(array $fieldDefinition): ResolveMissingFieldEvent + { + return new ResolveMissingFieldEvent( + $this->getContent(), + new FieldDefinition($fieldDefinition), + 'eng-GB' + ); + } +} diff --git a/tests/lib/Persistence/Legacy/Content/MapperTest.php b/tests/lib/Persistence/Legacy/Content/MapperTest.php index 806767f9e4..1caaac1715 100644 --- a/tests/lib/Persistence/Legacy/Content/MapperTest.php +++ b/tests/lib/Persistence/Legacy/Content/MapperTest.php @@ -20,8 +20,13 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Relation as RelationValue; use Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter; use Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry as Registry; +use Ibexa\Core\Persistence\Legacy\Content\Gateway; use Ibexa\Core\Persistence\Legacy\Content\Mapper; +use Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber; use Ibexa\Core\Persistence\Legacy\Content\StorageFieldValue; +use Ibexa\Core\Persistence\Legacy\Content\StorageRegistry; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @covers \Ibexa\Core\Persistence\Legacy\Content\Mapper @@ -87,8 +92,8 @@ public function testCreateVersionInfoForContent() ], $versionInfo ); - $this->assertGreaterThanOrEqual($time, $versionInfo->creationDate); - $this->assertGreaterThanOrEqual($time, $versionInfo->modificationDate); + self::assertGreaterThanOrEqual($time, $versionInfo->creationDate); + self::assertGreaterThanOrEqual($time, $versionInfo->modificationDate); } /** @@ -140,10 +145,15 @@ public function testConvertToStorageValue() $field->type = 'some-type'; $field->value = new FieldValue(); - $mapper = new Mapper($reg, $this->getLanguageHandler()); + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher(), + ); $res = $mapper->convertToStorageValue($field); - $this->assertInstanceOf( + self::assertInstanceOf( StorageFieldValue::class, $res ); @@ -154,85 +164,202 @@ public function testExtractContentFromRows() $rowsFixture = $this->getContentExtractFixture(); $nameRowsFixture = $this->getNamesExtractFixture(); - $convMock = $this->createMock(Converter::class); - $convMock->expects($this->exactly(count($rowsFixture))) - ->method('toFieldValue') - ->with( - $this->isInstanceOf( - StorageFieldValue::class - ) - )->will( - $this->returnValue( - new FieldValue() - ) - ); + $contentType = $this->getContentTypeFromRows($rowsFixture); - $reg = new Registry( - [ - 'ezauthor' => $convMock, - 'ezstring' => $convMock, - 'ezboolean' => $convMock, - 'ezimage' => $convMock, - 'ezdatetime' => $convMock, - 'ezkeyword' => $convMock, - ] + $contentTypeHandlerMock = $this->getContentTypeHandler(); + $contentTypeHandlerMock->method('load')->willReturn($contentType); + + $reg = $this->getFieldRegistry([ + 'ezauthor', + 'ezstring', + 'ezboolean', + 'ezimage', + 'ezdatetime', + 'ezkeyword', + ], count($rowsFixture) - 1); + + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $contentTypeHandlerMock, + $this->getEventDispatcher() ); + $result = $mapper->extractContentFromRows($rowsFixture, $nameRowsFixture); + + $expected = [$this->getContentExtractReference()]; - $mapper = new Mapper($reg, $this->getLanguageHandler()); + self::assertEquals( + $expected, + $result + ); + } + + public function testExtractContentFromRowsWithNewFieldDefinitions(): void + { + $rowsFixture = $this->getContentExtractFixture(); + $nameRowsFixture = $this->getNamesExtractFixture(); + + $contentType = $this->getContentTypeFromRows($rowsFixture); + $contentType->fieldDefinitions[] = new Content\Type\FieldDefinition([ + 'fieldType' => 'eznumber', + ]); + + $contentTypeHandlerMock = $this->getContentTypeHandler(); + $contentTypeHandlerMock->method('load')->willReturn($contentType); + + $reg = $this->getFieldRegistry([ + 'ezauthor', + 'ezstring', + 'ezboolean', + 'ezimage', + 'ezdatetime', + 'ezkeyword', + 'eznumber', + ], count($rowsFixture) - 1); + + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $contentTypeHandlerMock, + $this->getEventDispatcher() + ); $result = $mapper->extractContentFromRows($rowsFixture, $nameRowsFixture); - $this->assertEquals( + $expectedContent = $this->getContentExtractReference(); + $expectedContent->fields[] = new Field([ + 'type' => 'eznumber', + 'languageCode' => 'eng-US', + 'value' => new FieldValue(), + 'versionNo' => 2, + ]); + + self::assertEquals( [ - $this->getContentExtractReference(), + $expectedContent, ], $result ); } - public function testExtractContentFromRowsMultipleVersions() + public function testExtractContentFromRowsWithRemovedFieldDefinitions(): void { - $convMock = $this->createMock(Converter::class); - $convMock->expects($this->any()) - ->method('toFieldValue') - ->will($this->returnValue(new FieldValue())); + $rowsFixture = $this->getContentExtractFixture(); + $nameRowsFixture = $this->getNamesExtractFixture(); + + $contentType = $this->getContentTypeFromRows($rowsFixture); + $contentType->fieldDefinitions = array_filter( + $contentType->fieldDefinitions, + static function (Content\Type\FieldDefinition $fieldDefinition): bool { + // ref. fixtures, ezauthor + return $fieldDefinition->id !== 185; + } + ); + + $contentTypeHandlerMock = $this->getContentTypeHandler(); + $contentTypeHandlerMock->method('load')->willReturn($contentType); + + $reg = $this->getFieldRegistry([ + 'ezstring', + 'ezboolean', + 'ezimage', + 'ezdatetime', + 'ezkeyword', + ], count($rowsFixture) - 2); + + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $contentTypeHandlerMock, + $this->getEventDispatcher() + ); + $result = $mapper->extractContentFromRows($rowsFixture, $nameRowsFixture); - $reg = new Registry( + $expectedContent = $this->getContentExtractReference(); + $expectedContent->fields = array_values( + array_filter($expectedContent->fields, static function (Field $field): bool { + return $field->fieldDefinitionId !== 185; + }) + ); + + self::assertEquals( [ - 'ezstring' => $convMock, - 'ezdatetime' => $convMock, - ] + $expectedContent, + ], + $result ); + } + + public function testExtractContentFromRowsMultipleVersions() + { + $reg = $this->getFieldRegistry([ + 'ezstring', + 'ezdatetime', + ]); $rowsFixture = $this->getMultipleVersionsExtractFixture(); $nameRowsFixture = $this->getMultipleVersionsNamesExtractFixture(); - $mapper = new Mapper($reg, $this->getLanguageHandler()); + $contentType = $this->getContentTypeFromRows($rowsFixture); + + $contentTypeHandlerMock = $this->getContentTypeHandler(); + $contentTypeHandlerMock->method('load')->willReturn($contentType); + + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $contentTypeHandlerMock, + $this->getEventDispatcher() + ); $result = $mapper->extractContentFromRows($rowsFixture, $nameRowsFixture); - $this->assertCount( + self::assertCount( 2, $result ); - $this->assertEquals( + self::assertEquals( 11, $result[0]->versionInfo->contentInfo->id ); - $this->assertEquals( + self::assertEquals( 11, $result[1]->versionInfo->contentInfo->id ); - $this->assertEquals( + self::assertEquals( 1, $result[0]->versionInfo->versionNo ); - $this->assertEquals( + self::assertEquals( 2, $result[1]->versionInfo->versionNo ); } + /** + * @param string[] $fieldTypeIdentifiers + */ + private function getFieldRegistry( + array $fieldTypeIdentifiers = [], + ?int $expectedConverterCalls = null + ): Registry { + $converterMock = $this->createMock(Converter::class); + $converterMock->expects( + $expectedConverterCalls === null + ? self::any() + : self::exactly($expectedConverterCalls) + ) + ->method('toFieldValue') + ->willReturn(new FieldValue()); + + $converters = []; + foreach ($fieldTypeIdentifiers as $fieldTypeIdentifier) { + $converters[$fieldTypeIdentifier] = $converterMock; + } + + return new Registry($converters); + } + public function testCreateCreateStructFromContent() { $time = time(); @@ -242,7 +369,7 @@ public function testCreateCreateStructFromContent() $struct = $mapper->createCreateStructFromContent($content); - $this->assertInstanceOf(CreateStruct::class, $struct); + self::assertInstanceOf(CreateStruct::class, $struct); return [ 'original' => $content, @@ -279,7 +406,7 @@ public function testCreateCreateStructFromContentBasicProperties($data) */ public function testCreateCreateStructFromContentParentLocationsEmpty($data) { - $this->assertEquals( + self::assertEquals( [], $data['result']->locations ); @@ -290,7 +417,7 @@ public function testCreateCreateStructFromContentParentLocationsEmpty($data) */ public function testCreateCreateStructFromContentFieldCount($data) { - $this->assertEquals( + self::assertEquals( count($data['original']->fields), count($data['result']->fields) ); @@ -302,7 +429,7 @@ public function testCreateCreateStructFromContentFieldCount($data) public function testCreateCreateStructFromContentFieldsNoId($data) { foreach ($data['result']->fields as $field) { - $this->assertNull($field->id); + self::assertNull($field->id); } } @@ -314,7 +441,7 @@ public function testExtractRelationsFromRows() $res = $mapper->extractRelationsFromRows($rows); - $this->assertEquals( + self::assertEquals( $this->getRelationExtractReference(), $res ); @@ -330,7 +457,7 @@ public function testCreateCreateStructFromContentWithPreserveOriginalLanguage() $struct = $mapper->createCreateStructFromContent($content, true); - $this->assertInstanceOf(CreateStruct::class, $struct); + self::assertInstanceOf(CreateStruct::class, $struct); $this->assertStructsEqual($content->versionInfo->contentInfo, $struct, ['sectionId', 'ownerId']); self::assertNotEquals($content->versionInfo->contentInfo->remoteId, $struct->remoteId); self::assertSame($content->versionInfo->contentInfo->contentTypeId, $struct->typeId); @@ -351,7 +478,9 @@ public function testExtractContentInfoFromRow(array $fixtures, $prefix) $contentInfoReference = $this->getContentExtractReference()->versionInfo->contentInfo; $mapper = new Mapper( $this->getValueConverterRegistryMock(), - $this->getLanguageHandler() + $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher() ); self::assertEquals($contentInfoReference, $mapper->extractContentInfoFromRow($fixtures, $prefix)); } @@ -508,7 +637,9 @@ protected function getMapper($valueConverter = null) { return new Mapper( $this->getValueConverterRegistryMock(), - $this->getLanguageHandler() + $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher() ); } @@ -523,6 +654,10 @@ protected function getValueConverterRegistryMock() $this->valueConverterRegistryMock = $this->getMockBuilder(Registry::class) ->setMethods([]) ->getMock(); + + $this->valueConverterRegistryMock + ->method('getConverter') + ->willReturn($this->createMock(Converter::class)); } return $this->valueConverterRegistryMock; @@ -544,6 +679,20 @@ protected function getRelationCreateStructFixture(): RelationCreateStruct return $struct; } + protected function getEventDispatcher(): EventDispatcherInterface + { + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber( + new ResolveVirtualFieldSubscriber( + $this->getValueConverterRegistryMock(), + $this->createMock(StorageRegistry::class), + $this->createMock(Gateway::class) + ) + ); + + return $eventDispatcher; + } + /** * Returns a language handler mock. * @@ -603,6 +752,41 @@ static function ($languageCode) use ($languages) { return $this->languageHandler; } + + /** + * @return \Ibexa\Contracts\Core\Persistence\Content\Type\Handler|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getContentTypeHandler(): Content\Type\Handler + { + return $this->createMock(Content\Type\Handler::class); + } + + /** + * @param array> $rows + */ + protected function getContentTypeFromRows(array $rows): Content\Type + { + $contentType = new Content\Type(); + $fieldDefinitions = []; + + foreach ($rows as $row) { + $fieldDefinitionId = $row['ezcontentobject_attribute_contentclassattribute_id']; + $fieldType = $row['ezcontentobject_attribute_data_type_string']; + + if (isset($fieldDefinitions[$fieldDefinitionId])) { + continue; + } + + $fieldDefinitions[$fieldDefinitionId] = new Content\Type\FieldDefinition([ + 'id' => $fieldDefinitionId, + 'fieldType' => $fieldType, + ]); + } + + $contentType->fieldDefinitions = array_values($fieldDefinitions); + + return $contentType; + } } class_alias(MapperTest::class, 'eZ\Publish\Core\Persistence\Legacy\Tests\Content\MapperTest'); diff --git a/tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php b/tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php index 3ea01062bc..2ef2185b43 100644 --- a/tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php +++ b/tests/lib/Persistence/Legacy/Content/StorageHandlerTest.php @@ -19,35 +19,17 @@ */ class StorageHandlerTest extends TestCase { - /** - * StorageRegistry mock. - * - * @var \Ibexa\Core\Persistence\Legacy\Content\StorageRegistry - */ - protected $storageRegistryMock; + /** @var \Ibexa\Core\Persistence\Legacy\Content\StorageRegistry&\PHPUnit\Framework\MockObject\MockObject */ + protected StorageRegistry $storageRegistryMock; - /** - * StorageHandler to test. - * - * @var \Ibexa\Core\Persistence\Legacy\Content\StorageHandler - */ - protected $storageHandler; + protected StorageHandler $storageHandler; - /** - * Mock for external storage. - * - * @var \Ibexa\Contracts\Core\FieldType\FieldStorage - */ - protected $storageMock; + /** @var \Ibexa\Contracts\Core\FieldType\FieldStorage&\PHPUnit\Framework\MockObject\MockObject */ + protected FieldStorage $storageMock; - /** - * Mock for versionInfo. - * - * @var \Ibexa\Core\Repository\Values\Content\VersionInfo - */ - protected $versionInfoMock; + protected VersionInfo $versionInfoMock; - public function testStoreFieldData() + public function testStoreFieldData(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -73,7 +55,7 @@ public function testStoreFieldData() $handler->storeFieldData($this->getVersionInfoMock(), $field); } - public function testGetFieldDataAvailable() + public function testGetFieldDataAvailable(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -95,6 +77,7 @@ public function testGetFieldDataAvailable() ->will($this->returnValue($storageMock)); $field = new Field(); + $field->id = 123; $field->type = 'foobar'; $field->value = new FieldValue(); @@ -102,7 +85,7 @@ public function testGetFieldDataAvailable() $handler->getFieldData($this->getVersionInfoMock(), $field); } - public function testGetFieldDataNotAvailable() + public function testGetFieldDataNotAvailable(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -118,6 +101,31 @@ public function testGetFieldDataNotAvailable() ->with($this->equalTo('foobar')) ->will($this->returnValue($storageMock)); + $field = new Field(); + $field->id = 123; + $field->type = 'foobar'; + $field->value = new FieldValue(); + + $handler = $this->getStorageHandler(); + $handler->getFieldData($this->getVersionInfoMock(), $field); + } + + public function testGetFieldDataNotAvailableForVirtualField(): void + { + $storageMock = $this->getStorageMock(); + $storageRegistryMock = $this->getStorageRegistryMock(); + + $storageMock->expects(self::never()) + ->method('hasFieldData'); + + $storageMock->expects(self::never()) + ->method('getFieldData'); + + $storageRegistryMock->expects(self::once()) + ->method('getStorage') + ->with(self::equalTo('foobar')) + ->willReturn($storageMock); + $field = new Field(); $field->type = 'foobar'; $field->value = new FieldValue(); @@ -126,7 +134,7 @@ public function testGetFieldDataNotAvailable() $handler->getFieldData($this->getVersionInfoMock(), $field); } - public function testDeleteFieldData() + public function testDeleteFieldData(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -153,7 +161,7 @@ public function testDeleteFieldData() * * @return \Ibexa\Core\Persistence\Legacy\Content\StorageHandler */ - protected function getStorageHandler() + protected function getStorageHandler(): StorageHandler { if (!isset($this->storageHandler)) { $this->storageHandler = new StorageHandler( @@ -168,9 +176,9 @@ protected function getStorageHandler() /** * Returns a context mock. * - * @return array + * @return int[] */ - protected function getContextMock() + protected function getContextMock(): array { return [23, 42]; } @@ -178,9 +186,9 @@ protected function getContextMock() /** * Returns a StorageRegistry mock. * - * @return \Ibexa\Core\Persistence\Legacy\Content\StorageRegistry + * @return \Ibexa\Core\Persistence\Legacy\Content\StorageRegistry&\PHPUnit\Framework\MockObject\MockObject */ - protected function getStorageRegistryMock() + protected function getStorageRegistryMock(): StorageRegistry { if (!isset($this->storageRegistryMock)) { $this->storageRegistryMock = $this->getMockBuilder(StorageRegistry::class) @@ -195,9 +203,9 @@ protected function getStorageRegistryMock() /** * Returns a Storage mock. * - * @return \Ibexa\Contracts\Core\FieldType\FieldStorage + * @return \Ibexa\Contracts\Core\FieldType\FieldStorage&\PHPUnit\Framework\MockObject\MockObject */ - protected function getStorageMock() + protected function getStorageMock(): FieldStorage { if (!isset($this->storageMock)) { $this->storageMock = $this->createMock(FieldStorage::class); @@ -206,7 +214,7 @@ protected function getStorageMock() return $this->storageMock; } - protected function getVersionInfoMock() + protected function getVersionInfoMock(): VersionInfo { if (!isset($this->versionInfoMock)) { $this->versionInfoMock = $this->createMock(VersionInfo::class); diff --git a/tests/lib/Persistence/Legacy/Content/Type/ContentTypeHandlerTest.php b/tests/lib/Persistence/Legacy/Content/Type/ContentTypeHandlerTest.php index 2226bc1508..23bf963870 100644 --- a/tests/lib/Persistence/Legacy/Content/Type/ContentTypeHandlerTest.php +++ b/tests/lib/Persistence/Legacy/Content/Type/ContentTypeHandlerTest.php @@ -1005,12 +1005,9 @@ public function testPublish() $this->returnValue(new Type()) ); - $updateHandlerMock->expects($this->once()) - ->method('updateContentObjects') - ->with( - $this->isInstanceOf(Type::class), - $this->isInstanceOf(Type::class) - ); + $updateHandlerMock->expects($this->never()) + ->method('updateContentObjects'); + $updateHandlerMock->expects($this->once()) ->method('deleteOldType') ->with( diff --git a/tests/lib/Persistence/Legacy/Content/Type/Gateway/DoctrineDatabaseTest.php b/tests/lib/Persistence/Legacy/Content/Type/Gateway/DoctrineDatabaseTest.php index f6b066ea9a..f06999a73e 100644 --- a/tests/lib/Persistence/Legacy/Content/Type/Gateway/DoctrineDatabaseTest.php +++ b/tests/lib/Persistence/Legacy/Content/Type/Gateway/DoctrineDatabaseTest.php @@ -462,7 +462,6 @@ public function testInsertType($column, $expectation) public static function getTypeCreationContentClassNameExpectations() { return [ - ['contentclass_id', [1, 1]], ['contentclass_version', [0, 0]], ['language_id', [3, 4]], ['language_locale', ['eng-US', 'eng-GB']], diff --git a/tests/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabaseTest.php b/tests/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabaseTest.php index b051d3f509..dd2a5dd2f6 100644 --- a/tests/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabaseTest.php +++ b/tests/lib/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabaseTest.php @@ -37,23 +37,11 @@ public function testUpdateContentObjects() $updaterMock = $this->getContentUpdaterMock(); - $updaterMock->expects($this->once()) - ->method('determineActions') - ->with( - $this->isInstanceOf( - Type::class - ), - $this->isInstanceOf( - Type::class - ) - )->will($this->returnValue([])); - - $updaterMock->expects($this->once()) - ->method('applyUpdates') - ->with( - $this->equalTo(23), - $this->equalTo([]) - ); + $updaterMock->expects($this->never()) + ->method('determineActions'); + + $updaterMock->expects($this->never()) + ->method('applyUpdates'); $types = $this->getTypeFixtures(); @@ -121,10 +109,7 @@ protected function getTypeFixtures() */ protected function getUpdateHandler() { - return new DoctrineDatabase( - $this->getGatewayMock(), - $this->getContentUpdaterMock() - ); + return new DoctrineDatabase($this->getGatewayMock()); } /** diff --git a/tests/lib/Persistence/Legacy/Content/_fixtures/extract_content_from_rows_result.php b/tests/lib/Persistence/Legacy/Content/_fixtures/extract_content_from_rows_result.php index 5873f3f57a..03f9783faa 100644 --- a/tests/lib/Persistence/Legacy/Content/_fixtures/extract_content_from_rows_result.php +++ b/tests/lib/Persistence/Legacy/Content/_fixtures/extract_content_from_rows_result.php @@ -123,14 +123,4 @@ $content->fields[] = $field; -$field = new Field(); -$field->id = 4000; -$field->fieldDefinitionId = 193; -$field->type = 'ezkeyword'; -$field->value = new FieldValue(); -$field->languageCode = 'eng-GB'; -$field->versionNo = 2; - -$content->fields[] = $field; - return $content; diff --git a/tests/lib/Persistence/Legacy/SharedGateway/GatewayFactoryTest.php b/tests/lib/Persistence/Legacy/SharedGateway/GatewayFactoryTest.php index a75aab39c8..090c366bf9 100644 --- a/tests/lib/Persistence/Legacy/SharedGateway/GatewayFactoryTest.php +++ b/tests/lib/Persistence/Legacy/SharedGateway/GatewayFactoryTest.php @@ -30,7 +30,7 @@ final class GatewayFactoryTest extends TestCase public function setUp(): void { $gateways = [ - 'sqlite' => new SqliteGateway($this->createMock(Connection::class)), + 'sqlite' => new SqliteGateway(), ]; $this->factory = new GatewayFactory( diff --git a/tests/lib/Persistence/Legacy/TestCase.php b/tests/lib/Persistence/Legacy/TestCase.php index 72b9593222..887861cfe7 100644 --- a/tests/lib/Persistence/Legacy/TestCase.php +++ b/tests/lib/Persistence/Legacy/TestCase.php @@ -114,7 +114,7 @@ final public function getSharedGateway(): SharedGateway\Gateway $factory = new SharedGateway\GatewayFactory( new SharedGateway\DatabasePlatform\FallbackGateway($connection), [ - 'sqlite' => new SharedGateway\DatabasePlatform\SqliteGateway($connection), + 'sqlite' => new SharedGateway\DatabasePlatform\SqliteGateway(), ] ); diff --git a/tests/lib/Search/Legacy/Content/AbstractTestCase.php b/tests/lib/Search/Legacy/Content/AbstractTestCase.php index ca282f7768..0ae8a3bf37 100644 --- a/tests/lib/Search/Legacy/Content/AbstractTestCase.php +++ b/tests/lib/Search/Legacy/Content/AbstractTestCase.php @@ -9,12 +9,17 @@ use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as SPIContentTypeHandler; use Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter; use Ibexa\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry; +use Ibexa\Core\Persistence\Legacy\Content\Gateway; +use Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber; +use Ibexa\Core\Persistence\Legacy\Content\StorageRegistry; use Ibexa\Core\Persistence\Legacy\Content\Type\Gateway\DoctrineDatabase as ContentTypeGateway; use Ibexa\Core\Persistence\Legacy\Content\Type\Handler as ContentTypeHandler; use Ibexa\Core\Persistence\Legacy\Content\Type\Mapper as ContentTypeMapper; use Ibexa\Core\Persistence\Legacy\Content\Type\StorageDispatcherInterface; use Ibexa\Core\Persistence\Legacy\Content\Type\Update\Handler as ContentTypeUpdateHandler; use Ibexa\Tests\Core\Persistence\Legacy\Content\LanguageAwareTestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Abstract test suite for legacy search. @@ -119,6 +124,20 @@ protected function getConverterRegistry() return $this->converterRegistry; } + + protected function getEventDispatcher(): EventDispatcherInterface + { + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber( + new ResolveVirtualFieldSubscriber( + $this->getConverterRegistry(), + $this->createMock(StorageRegistry::class), + $this->createMock(Gateway::class) + ) + ); + + return $eventDispatcher; + } } class_alias(AbstractTestCase::class, 'eZ\Publish\Core\Search\Legacy\Tests\Content\AbstractTestCase'); diff --git a/tests/lib/Search/Legacy/Content/HandlerContentSortTest.php b/tests/lib/Search/Legacy/Content/HandlerContentSortTest.php index 1194ceb488..ec24e4efcb 100644 --- a/tests/lib/Search/Legacy/Content/HandlerContentSortTest.php +++ b/tests/lib/Search/Legacy/Content/HandlerContentSortTest.php @@ -101,6 +101,8 @@ protected function getContentMapperMock() [ $this->getFieldRegistry(), $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher(), ] ) ->setMethods(['extractContentInfoFromRows']) diff --git a/tests/lib/Search/Legacy/Content/HandlerContentTest.php b/tests/lib/Search/Legacy/Content/HandlerContentTest.php index b5166e9a09..3f37fe0a27 100644 --- a/tests/lib/Search/Legacy/Content/HandlerContentTest.php +++ b/tests/lib/Search/Legacy/Content/HandlerContentTest.php @@ -200,6 +200,8 @@ protected function getContentMapperMock() [ $this->getConverterRegistry(), $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher(), ] ) ->setMethods(['extractContentInfoFromRows'])