From 98b7b50e60124a3bc7e6d79c123d57a31d0427bd Mon Sep 17 00:00:00 2001 From: Slawomir Dolzycki-Uchto Date: Fri, 10 May 2024 15:08:42 +0200 Subject: [PATCH] IBX-5388: Fixed performance issues of content updates after field changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For more details see https://issues.ibexa.co/browse/IBX-5388 and https://github.com/ezsystems/ezplatform-kernel/pull/397 Key changes: * Fixed performance issues of content updates after field definition changes * Made DefaultDataFieldStorage extend FieldStorage * [Tests] Aligned test coverage with the changes --------- Co-Authored-By: Paweł Niedzielski Co-Authored-By: Andrew Longosz --- .../Tests/ContentTypeServiceTest.php | 61 ---- .../API/Repository/Values/Content/Field.php | 15 +- .../Legacy/Content/FieldHandler.php | 6 +- .../Persistence/Legacy/Content/Mapper.php | 186 ++++++++-- .../Legacy/Content/StorageHandler.php | 2 +- .../Legacy/Content/Type/Handler.php | 1 - .../Type/Update/Handler/DoctrineDatabase.php | 12 +- .../DatabasePlatform/SqliteGateway.php | 32 +- .../Legacy/Tests/Content/MapperTest.php | 289 ++++++++++++--- .../Tests/Content/StorageHandlerTest.php | 60 ++-- .../Content/Type/ContentTypeHandlerTest.php | 9 +- .../Type/Gateway/DoctrineDatabaseTest.php | 1 - .../Update/Handler/DoctrineDatabaseTest.php | 27 +- .../extract_content_from_rows_result.php | 10 - .../SharedGateway/GatewayFactoryTest.php | 2 +- .../Persistence/Legacy/Tests/TestCase.php | 2 +- .../Legacy/Tests/Content/AbstractTestCase.php | 19 + .../Tests/Content/HandlerContentSortTest.php | 2 + .../Tests/Content/HandlerContentTest.php | 2 + .../storage_engines/legacy/content.yml | 10 + .../storage_engines/legacy/content_type.yml | 1 - .../settings/tests/integration_legacy.yml | 1 + .../Event/Mapper/ResolveMissingFieldEvent.php | 79 ++++ .../FieldType/DefaultDataFieldStorage.php | 25 ++ .../Mapper/ResolveVirtualFieldSubscriber.php | 215 +++++++++++ .../ResolveVirtualFieldSubscriberTest.php | 336 ++++++++++++++++++ 26 files changed, 1164 insertions(+), 241 deletions(-) create mode 100644 src/contracts/Event/Mapper/ResolveMissingFieldEvent.php create mode 100644 src/contracts/FieldType/DefaultDataFieldStorage.php create mode 100644 src/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriber.php create mode 100644 tests/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriberTest.php diff --git a/eZ/Publish/API/Repository/Tests/ContentTypeServiceTest.php b/eZ/Publish/API/Repository/Tests/ContentTypeServiceTest.php index b76bab4f72..ad095d5632 100644 --- a/eZ/Publish/API/Repository/Tests/ContentTypeServiceTest.php +++ b/eZ/Publish/API/Repository/Tests/ContentTypeServiceTest.php @@ -15,7 +15,6 @@ use eZ\Publish\API\Repository\Values\ContentType\ContentTypeGroup; use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition; use eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionCollection as APIFieldDefinitionCollection; -use eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionCreateStruct; use eZ\Publish\API\Repository\Values\Translation\Message; use eZ\Publish\Core\FieldType\TextLine\Value as TextLineValue; @@ -2021,66 +2020,6 @@ public function testRemoveFieldDefinitionRemovesFieldFromContentRemoved($data) ); } - /** - * @covers \eZ\Publish\API\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/eZ/Publish/API/Repository/Values/Content/Field.php b/eZ/Publish/API/Repository/Values/Content/Field.php index 83c2717df0..a4af0a3215 100644 --- a/eZ/Publish/API/Repository/Values/Content/Field.php +++ b/eZ/Publish/API/Repository/Values/Content/Field.php @@ -24,9 +24,10 @@ 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 int + * @var int|null */ protected $id; @@ -58,7 +59,7 @@ class Field extends ValueObject */ protected $fieldTypeIdentifier; - public function getId(): int + public function getId(): ?int { return $this->id; } @@ -85,4 +86,12 @@ public function getFieldTypeIdentifier(): string { return $this->fieldTypeIdentifier; } + + /** + * @phpstan-assert-if-true !null $this->getId() + */ + public function isVirtual(): bool + { + return null === $this->id; + } } diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/FieldHandler.php b/eZ/Publish/Core/Persistence/Legacy/Content/FieldHandler.php index cd3440db7a..dba486b78e 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/FieldHandler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/FieldHandler.php @@ -148,9 +148,13 @@ protected function getEmptyField(FieldDefinition $fieldDefinition, $languageCode * * @param \eZ\Publish\SPI\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/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php b/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php index 20f3708b93..dafce22757 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php @@ -16,12 +16,45 @@ use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler; use eZ\Publish\SPI\Persistence\Content\Relation; use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct; +use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler; use eZ\Publish\SPI\Persistence\Content\VersionInfo; +use Ibexa\Contracts\Core\Event\Mapper\ResolveMissingFieldEvent; +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, \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition, + * > + * > + * > + * > + * @phpstan-type TVersionedFieldMap array< + * int, array< + * int, array< + * int, \eZ\Publish\SPI\Persistence\Content\Field, + * > + * > + * > + * @phpstan-type TVersionedNameMap array< + * int, array< + * int, array< + * string, array + * > + * > + * > + * @phpstan-type TContentInfoMap array + * @phpstan-type TVersionInfoMap array< + * int, array< + * int, \eZ\Publish\SPI\Persistence\Content\VersionInfo, + * > + * > */ class Mapper { @@ -40,15 +73,25 @@ class Mapper protected $languageHandler; /** - * Creates a new mapper. - * - * @param \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry $converterRegistry - * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler + * @var \eZ\Publish\SPI\Persistence\Content\Type\Handler */ - public function __construct(Registry $converterRegistry, LanguageHandler $languageHandler) - { + private $contentTypeHandler; + + /** + * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface + */ + private $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 +217,122 @@ public function convertToStorageValue(Field $field) * * "$tableName_$columnName" * - * @param array $rows - * @param array $nameRows + * @param array> $rows + * @param array> $nameRows + * @param string $prefix * * @return \eZ\Publish\SPI\Persistence\Content[] + * + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException */ - 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 = $row["{$prefix}name_content_translation"]; + $versionedNameData[$contentId][$versionNo][$languageCode] = $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 \eZ\Publish\SPI\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,6 +340,43 @@ public function extractContentFromRows(array $rows, array $nameRows, $prefix = ' return $results; } + /** + * @phpstan-return TVersionedLanguageFieldDefinitionsMap + * + * @throws \eZ\Publish\API\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 = $fieldDefinition->id; + $fieldDefinitions[$contentId][$versionId][$languageCode][$id] = $fieldDefinition; + } + } + } + + return $fieldDefinitions; + } + /** * Extracts a ContentInfo object from $row. * @@ -251,7 +394,6 @@ public function extractContentInfoFromRow(array $row, $prefix = '', $treePrefix $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"]; diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/StorageHandler.php b/eZ/Publish/Core/Persistence/Legacy/Content/StorageHandler.php index 72c4bb7e8d..4101063fad 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/StorageHandler.php +++ b/eZ/Publish/Core/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/eZ/Publish/Core/Persistence/Legacy/Content/Type/Handler.php b/eZ/Publish/Core/Persistence/Legacy/Content/Type/Handler.php index dcb3f423e9..54068a5393 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Type/Handler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Type/Handler.php @@ -592,7 +592,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/eZ/Publish/Core/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php index 7a5bc6ccc1..9351df00bf 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Type/Update/Handler/DoctrineDatabase.php @@ -8,7 +8,6 @@ namespace eZ\Publish\Core\Persistence\Legacy\Content\Type\Update\Handler; -use eZ\Publish\Core\Persistence\Legacy\Content\Type\ContentUpdater; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Gateway; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Update\Handler; use eZ\Publish\SPI\Persistence\Content\Type; @@ -23,21 +22,14 @@ final class DoctrineDatabase extends Handler /** @var \eZ\Publish\Core\Persistence\Legacy\Content\Type\Gateway */ protected $contentTypeGateway; - /** @var \eZ\Publish\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/eZ/Publish/Core/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php b/eZ/Publish/Core/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php index fcb081f759..fea446be49 100644 --- a/eZ/Publish/Core/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php +++ b/eZ/Publish/Core/Persistence/Legacy/SharedGateway/DatabasePlatform/SqliteGateway.php @@ -8,8 +8,6 @@ namespace eZ\Publish\Core\Persistence\Legacy\SharedGateway\DatabasePlatform; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\FetchMode; use eZ\Publish\Core\Base\Exceptions\DatabaseException; use eZ\Publish\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/eZ/Publish/Core/Persistence/Legacy/Tests/Content/MapperTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/MapperTest.php index 99a721f147..f4e59b9b9b 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/MapperTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/MapperTest.php @@ -8,10 +8,13 @@ use function count; use eZ\Publish\API\Repository\Values\Content\Relation as RelationValue; +use eZ\Publish\Core\Persistence\Legacy\Bookmark\Handler; use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter; use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry as Registry; +use eZ\Publish\Core\Persistence\Legacy\Content\Gateway; use eZ\Publish\Core\Persistence\Legacy\Content\Mapper; use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue; +use eZ\Publish\Core\Persistence\Legacy\Content\StorageRegistry; use eZ\Publish\SPI\Persistence\Content; use eZ\Publish\SPI\Persistence\Content\ContentInfo; use eZ\Publish\SPI\Persistence\Content\CreateStruct; @@ -22,6 +25,9 @@ use eZ\Publish\SPI\Persistence\Content\Relation as SPIRelation; use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct; use eZ\Publish\SPI\Persistence\Content\VersionInfo; +use Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Test case for Mapper. @@ -92,8 +98,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); } /** @@ -149,10 +155,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 ); @@ -169,36 +180,126 @@ 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); + + $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()]; + + 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', + ]); - $reg = new Registry( + $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); + + $expectedContent = $this->getContentExtractReference(); + $expectedContent->fields[] = new Field([ + 'type' => 'eznumber', + 'languageCode' => 'eng-US', + 'value' => new FieldValue(), + 'versionNo' => 2, + ]); + + self::assertEquals( [ - 'ezauthor' => $convMock, - 'ezstring' => $convMock, - 'ezboolean' => $convMock, - 'ezimage' => $convMock, - 'ezdatetime' => $convMock, - 'ezkeyword' => $convMock, - ] + $expectedContent, + ], + $result ); + } + + public function testExtractContentFromRowsWithRemovedFieldDefinitions(): void + { + $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()); + $mapper = new Mapper( + $reg, + $this->getLanguageHandler(), + $contentTypeHandlerMock, + $this->getEventDispatcher() + ); $result = $mapper->extractContentFromRows($rowsFixture, $nameRowsFixture); - $this->assertEquals( + $expectedContent = $this->getContentExtractReference(); + $expectedContent->fields = array_values( + array_filter($expectedContent->fields, static function (Field $field): bool { + return $field->fieldDefinitionId !== 185; + }) + ); + + self::assertEquals( [ - $this->getContentExtractReference(), + $expectedContent, ], $result ); @@ -209,48 +310,75 @@ public function testExtractContentFromRows() */ public function testExtractContentFromRowsMultipleVersions() { - $convMock = $this->createMock(Converter::class); - $convMock->expects($this->any()) - ->method('toFieldValue') - ->will($this->returnValue(new FieldValue())); - - $reg = new Registry( - [ - 'ezstring' => $convMock, - 'ezdatetime' => $convMock, - ] - ); + $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); + } + /** * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Mapper::createCreateStructFromContent */ @@ -263,7 +391,7 @@ public function testCreateCreateStructFromContent() $struct = $mapper->createCreateStructFromContent($content); - $this->assertInstanceOf(CreateStruct::class, $struct); + self::assertInstanceOf(CreateStruct::class, $struct); return [ 'original' => $content, @@ -302,7 +430,7 @@ public function testCreateCreateStructFromContentBasicProperties($data) */ public function testCreateCreateStructFromContentParentLocationsEmpty($data) { - $this->assertEquals( + self::assertEquals( [], $data['result']->locations ); @@ -314,7 +442,7 @@ public function testCreateCreateStructFromContentParentLocationsEmpty($data) */ public function testCreateCreateStructFromContentFieldCount($data) { - $this->assertEquals( + self::assertEquals( count($data['original']->fields), count($data['result']->fields) ); @@ -327,7 +455,7 @@ public function testCreateCreateStructFromContentFieldCount($data) public function testCreateCreateStructFromContentFieldsNoId($data) { foreach ($data['result']->fields as $field) { - $this->assertNull($field->id); + self::assertNull($field->id); } } @@ -339,7 +467,7 @@ public function testExtractRelationsFromRows() $res = $mapper->extractRelationsFromRows($rows); - $this->assertEquals( + self::assertEquals( $this->getRelationExtractReference(), $res ); @@ -358,7 +486,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); @@ -380,7 +508,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)); } @@ -540,7 +670,9 @@ protected function getMapper($valueConverter = null) { return new Mapper( $this->getValueConverterRegistryMock(), - $this->getLanguageHandler() + $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher() ); } @@ -555,6 +687,10 @@ protected function getValueConverterRegistryMock() $this->valueConverterRegistryMock = $this->getMockBuilder(Registry::class) ->setMethods([]) ->getMock(); + + $this->valueConverterRegistryMock + ->method('getConverter') + ->willReturn($this->createMock(Converter::class)); } return $this->valueConverterRegistryMock; @@ -578,6 +714,20 @@ protected function getRelationCreateStructFixture() 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. * @@ -637,4 +787,39 @@ static function ($languageCode) use ($languages) { return $this->languageHandler; } + + /** + * @return \eZ\Publish\SPI\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; + } } diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/StorageHandlerTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/StorageHandlerTest.php index a32bf3fc37..72b6ea4598 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/StorageHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/StorageHandlerTest.php @@ -16,6 +16,8 @@ /** * Test case for Content Handler. + * + * @covers \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler */ class StorageHandlerTest extends TestCase { @@ -47,10 +49,7 @@ class StorageHandlerTest extends TestCase */ protected $versionInfoMock; - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler::storeFieldData - */ - public function testStoreFieldData() + public function testStoreFieldData(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -76,10 +75,7 @@ public function testStoreFieldData() $handler->storeFieldData($this->getVersionInfoMock(), $field); } - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler::getFieldData - */ - public function testGetFieldDataAvailable() + public function testGetFieldDataAvailable(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -101,6 +97,7 @@ public function testGetFieldDataAvailable() ->will($this->returnValue($storageMock)); $field = new Field(); + $field->id = 123; $field->type = 'foobar'; $field->value = new FieldValue(); @@ -108,10 +105,7 @@ public function testGetFieldDataAvailable() $handler->getFieldData($this->getVersionInfoMock(), $field); } - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler::getFieldData - */ - public function testGetFieldDataNotAvailable() + public function testGetFieldDataNotAvailable(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -128,6 +122,7 @@ public function testGetFieldDataNotAvailable() ->will($this->returnValue($storageMock)); $field = new Field(); + $field->id = 123; $field->type = 'foobar'; $field->value = new FieldValue(); @@ -135,10 +130,31 @@ public function testGetFieldDataNotAvailable() $handler->getFieldData($this->getVersionInfoMock(), $field); } - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler::deleteFieldData - */ - public function testDeleteFieldData() + 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(); + + $handler = $this->getStorageHandler(); + $handler->getFieldData($this->getVersionInfoMock(), $field); + } + + public function testDeleteFieldData(): void { $storageMock = $this->getStorageMock(); $storageRegistryMock = $this->getStorageRegistryMock(); @@ -165,7 +181,7 @@ public function testDeleteFieldData() * * @return \eZ\Publish\Core\Persistence\Legacy\Content\StorageHandler */ - protected function getStorageHandler() + protected function getStorageHandler(): StorageHandler { if (!isset($this->storageHandler)) { $this->storageHandler = new StorageHandler( @@ -180,9 +196,9 @@ protected function getStorageHandler() /** * Returns a context mock. * - * @return array + * @return int[] */ - protected function getContextMock() + protected function getContextMock(): array { return [23, 42]; } @@ -192,7 +208,7 @@ protected function getContextMock() * * @return \eZ\Publish\Core\Persistence\Legacy\Content\StorageRegistry */ - protected function getStorageRegistryMock() + protected function getStorageRegistryMock(): StorageRegistry { if (!isset($this->storageRegistryMock)) { $this->storageRegistryMock = $this->getMockBuilder(StorageRegistry::class) @@ -209,7 +225,7 @@ protected function getStorageRegistryMock() * * @return \eZ\Publish\SPI\FieldType\FieldStorage */ - protected function getStorageMock() + protected function getStorageMock(): FieldStorage { if (!isset($this->storageMock)) { $this->storageMock = $this->createMock(FieldStorage::class); @@ -218,7 +234,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/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/ContentTypeHandlerTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/ContentTypeHandlerTest.php index 081c62189e..a2eaf589d5 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/ContentTypeHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/ContentTypeHandlerTest.php @@ -1037,12 +1037,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/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Gateway/DoctrineDatabaseTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Gateway/DoctrineDatabaseTest.php index 45d13e4d56..524a334df8 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Gateway/DoctrineDatabaseTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Gateway/DoctrineDatabaseTest.php @@ -497,7 +497,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/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Update/Handler/DoctrineDatabaseTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Update/Handler/DoctrineDatabaseTest.php index df3acf4596..608ad8a80a 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Update/Handler/DoctrineDatabaseTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Type/Update/Handler/DoctrineDatabaseTest.php @@ -40,23 +40,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(); @@ -130,10 +118,7 @@ protected function getTypeFixtures() */ protected function getUpdateHandler() { - return new DoctrineDatabase( - $this->getGatewayMock(), - $this->getContentUpdaterMock() - ); + return new DoctrineDatabase($this->getGatewayMock()); } /** diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_result.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_result.php index 0bfc326130..9460576b1c 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_result.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_result.php @@ -119,14 +119,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/eZ/Publish/Core/Persistence/Legacy/Tests/SharedGateway/GatewayFactoryTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/SharedGateway/GatewayFactoryTest.php index 18856557f2..649da896b8 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/SharedGateway/GatewayFactoryTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/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/eZ/Publish/Core/Persistence/Legacy/Tests/TestCase.php b/eZ/Publish/Core/Persistence/Legacy/Tests/TestCase.php index 9a66f99734..055796815a 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/TestCase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/TestCase.php @@ -113,7 +113,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/eZ/Publish/Core/Search/Legacy/Tests/Content/AbstractTestCase.php b/eZ/Publish/Core/Search/Legacy/Tests/Content/AbstractTestCase.php index 1b795ae058..ab25ca43a7 100644 --- a/eZ/Publish/Core/Search/Legacy/Tests/Content/AbstractTestCase.php +++ b/eZ/Publish/Core/Search/Legacy/Tests/Content/AbstractTestCase.php @@ -8,12 +8,17 @@ use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter; use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry; +use eZ\Publish\Core\Persistence\Legacy\Content\Gateway; +use eZ\Publish\Core\Persistence\Legacy\Content\StorageRegistry; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Gateway\DoctrineDatabase as ContentTypeGateway; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Handler as ContentTypeHandler; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Mapper as ContentTypeMapper; use eZ\Publish\Core\Persistence\Legacy\Content\Type\Update\Handler as ContentTypeUpdateHandler; use eZ\Publish\Core\Persistence\Legacy\Tests\Content\LanguageAwareTestCase; use eZ\Publish\SPI\Persistence\Content\Type\Handler as SPIContentTypeHandler; +use Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Abstract test suite for legacy search. @@ -113,4 +118,18 @@ 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; + } } diff --git a/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentSortTest.php b/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentSortTest.php index b8b2e4c7a8..9d057cbe4b 100644 --- a/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentSortTest.php +++ b/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentSortTest.php @@ -101,6 +101,8 @@ protected function getContentMapperMock() [ $this->getFieldRegistry(), $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher(), ] ) ->setMethods(['extractContentInfoFromRows']) diff --git a/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentTest.php b/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentTest.php index 5701ba1d00..ef77af8f2f 100644 --- a/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentTest.php +++ b/eZ/Publish/Core/Search/Legacy/Tests/Content/HandlerContentTest.php @@ -200,6 +200,8 @@ protected function getContentMapperMock() [ $this->getConverterRegistry(), $this->getLanguageHandler(), + $this->getContentTypeHandler(), + $this->getEventDispatcher(), ] ) ->setMethods(['extractContentInfoFromRows']) diff --git a/eZ/Publish/Core/settings/storage_engines/legacy/content.yml b/eZ/Publish/Core/settings/storage_engines/legacy/content.yml index 8c402f1679..58c0de94b0 100644 --- a/eZ/Publish/Core/settings/storage_engines/legacy/content.yml +++ b/eZ/Publish/Core/settings/storage_engines/legacy/content.yml @@ -9,6 +9,16 @@ services: arguments: - "@ezpublish.persistence.legacy.field_value_converter.registry" - "@ezpublish.spi.persistence.legacy.language.handler" + - '@ezpublish.spi.persistence.legacy.content_type.handler' + - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' + + Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber: + arguments: + $converterRegistry: '@ezpublish.persistence.legacy.field_value_converter.registry' + $storageRegistry: '@ezpublish.persistence.external_storage_registry' + $contentGateway: '@ezpublish.persistence.legacy.content.gateway' + tags: + - { name: kernel.event_subscriber } ezpublish.persistence.legacy.content.gateway.inner: class: eZ\Publish\Core\Persistence\Legacy\Content\Gateway\DoctrineDatabase diff --git a/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml b/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml index b5f0385afd..951cc8f3fb 100644 --- a/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml +++ b/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml @@ -38,7 +38,6 @@ services: class: eZ\Publish\Core\Persistence\Legacy\Content\Type\Update\Handler\DoctrineDatabase arguments: - "@ezpublish.persistence.legacy.content_type.gateway" - - "@ezpublish.persistence.legacy.content_type.content_updater" ezpublish.persistence.legacy.content_type.update_handler: alias: ezpublish.persistence.legacy.content_type.update_handler.basic diff --git a/eZ/Publish/Core/settings/tests/integration_legacy.yml b/eZ/Publish/Core/settings/tests/integration_legacy.yml index d656f1b935..c2a96cec18 100644 --- a/eZ/Publish/Core/settings/tests/integration_legacy.yml +++ b/eZ/Publish/Core/settings/tests/integration_legacy.yml @@ -33,6 +33,7 @@ services: - ['addSubscriber', ['@eZ\Publish\Core\Search\Common\EventSubscriber\SectionEventSubscriber']] - ['addSubscriber', ['@eZ\Publish\Core\Search\Common\EventSubscriber\TrashEventSubscriber']] - ['addSubscriber', ['@eZ\Publish\Core\Search\Common\EventSubscriber\UserEventSubscriber']] + - ['addSubscriber', ['@Ibexa\Core\Persistence\Legacy\Content\Mapper\ResolveVirtualFieldSubscriber']] Doctrine\Common\EventManager: ~ diff --git a/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php b/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php new file mode 100644 index 0000000000..af0f0c2282 --- /dev/null +++ b/src/contracts/Event/Mapper/ResolveMissingFieldEvent.php @@ -0,0 +1,79 @@ + */ + private $context; + + /** @var \eZ\Publish\SPI\Persistence\Content\Field|null */ + private $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..ebb00182b5 --- /dev/null +++ b/src/contracts/FieldType/DefaultDataFieldStorage.php @@ -0,0 +1,25 @@ +$field value property with default data based on the external data. + * + * $field->value is a {@see \eZ\Publish\SPI\Persistence\Content\FieldValue} object. + * This value holds the data as a {@see \eZ\Publish\Core\FieldType\Value} based object, according to + * the field type (e.g. for TextLine, it will be a {@see \eZ\Publish\Core\FieldType\TextLine\Value} object). + */ + public function getDefaultFieldData(VersionInfo $versionInfo, Field $field): void; +} 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..2443586f34 --- /dev/null +++ b/src/lib/Persistence/Legacy/Content/Mapper/ResolveVirtualFieldSubscriber.php @@ -0,0 +1,215 @@ +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 \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter\Exception\NotFound + */ + public function persistExternalStorageField(ResolveMissingFieldEvent $event): void + { + $field = $event->getField(); + + if ($field !== null && $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 && $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 \eZ\Publish\API\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 \eZ\Publish\API\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 = null; + $storageValue->dataInt = null; + $storageValue->dataText = ''; + $storageValue->sortKeyInt = 0; + $storageValue->sortKeyString = ''; + + return $storageValue; + } +} 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..67e50f6496 --- /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' + ); + } +}