Skip to content

Commit

Permalink
IBX-5388: Fixed performance issues of content updates after field cha…
Browse files Browse the repository at this point in the history
…nges

For more details see https://issues.ibexa.co/browse/IBX-5388 and ezsystems/ezplatform-kernel#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 <Steveb-p@users.noreply.github.com>
Co-Authored-By: Andrew Longosz <alongosz@users.noreply.github.com>
  • Loading branch information
3 people committed May 29, 2024
1 parent b2f9e93 commit 82b486d
Show file tree
Hide file tree
Showing 15 changed files with 676 additions and 43 deletions.
79 changes: 79 additions & 0 deletions src/contracts/Event/Mapper/ResolveMissingFieldEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace Ibexa\Contracts\Core\Event\Mapper;

use eZ\Publish\SPI\Persistence\Content;
use eZ\Publish\SPI\Persistence\Content\Field;
use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition;
use Symfony\Contracts\EventDispatcher\Event;

final class ResolveMissingFieldEvent extends Event
{
/** @var \eZ\Publish\SPI\Persistence\Content */
private $content;

/** @var \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition */
private $fieldDefinition;

/** @var string */
private $languageCode;

/** @var array<mixed> */
private $context;

/** @var \eZ\Publish\SPI\Persistence\Content\Field|null */
private $field;

/**
* @param array<mixed> $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<mixed>
*/
public function getContext(): array
{
return $this->context;
}

public function setField(?Field $field): void
{
$this->field = $field;
}

public function getField(): ?Field
{
return $this->field;
}
}
25 changes: 25 additions & 0 deletions src/contracts/FieldType/DefaultDataFieldStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Core\FieldType;

use eZ\Publish\SPI\FieldType\FieldStorage;
use eZ\Publish\SPI\Persistence\Content\Field;
use eZ\Publish\SPI\Persistence\Content\VersionInfo;

interface DefaultDataFieldStorage extends FieldStorage
{
/**
* Populates <code>$field</code> value property with default data based on the external data.
*
* <code>$field->value</code> 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 <code>TextLine</code>, it will be a {@see \eZ\Publish\Core\FieldType\TextLine\Value} object).
*/
public function getDefaultFieldData(VersionInfo $versionInfo, Field $field): void;
}
6 changes: 5 additions & 1 deletion src/lib/Persistence/Legacy/Content/FieldHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Persistence\Legacy\Content\Mapper;

use eZ\Publish\Core\FieldType\NullStorage;
use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter\Exception\NotFound;
use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry;
use eZ\Publish\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
use eZ\Publish\Core\Persistence\Legacy\Content\StorageFieldValue;
use eZ\Publish\Core\Persistence\Legacy\Content\StorageRegistry;
use eZ\Publish\SPI\Persistence\Content\Field;
use eZ\Publish\SPI\Persistence\Content\FieldValue;
use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition;
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
use Ibexa\Contracts\Core\Event\Mapper\ResolveMissingFieldEvent;
use Ibexa\Contracts\Core\FieldType\DefaultDataFieldStorage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class ResolveVirtualFieldSubscriber implements EventSubscriberInterface
{
/** @var \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry */
private $converterRegistry;

/** @var \eZ\Publish\Core\Persistence\Legacy\Content\StorageRegistry */
private $storageRegistry;

/** @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway */
private $contentGateway;

public function __construct(
ConverterRegistry $converterRegistry,
StorageRegistry $storageRegistry,
ContentGateway $contentGateway
) {
$this->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;
}
}
2 changes: 1 addition & 1 deletion src/lib/Persistence/Legacy/Content/StorageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
1 change: 0 additions & 1 deletion src/lib/Persistence/Legacy/Content/Type/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 82b486d

Please sign in to comment.