Converting image fields to use the image asset field type in eZ Platform

Converting image fields to use the image asset field type in eZ Platform

One of the hallmark features of the eZ Platform Content Engine is its flexibility. Developers are free to construct their content model based on field types. eZ Platform ships with many core field types, and over time we've added more useful ones that you can take into use on your projects.

In eZ Platform 2.3 we added an image asset field type to complement the classic image one. The new type enables reuse by using dedicated image objects and relationships instead of an in-place image in a content object. A useful improvement, but switching field types from one to another can be tricky. Let's learn how to convert image fields to image assets automatically.

We will leave any automatic changes to the content structure out of scope here. The complete workflow for changing a single content type's image field type to an image asset field type is:

  • Add new Image Asset field to content type
  • Execute migration script (this is what we will build in this article)
  • Make necessary code and template changes to use the new field
  • Test, test, test!!!
  • Execute migration script again if data has changed
  • Remove Image field from content

To create our field type data migration script we will use a Symfony command, just as we did in an earlier blog post where we created an audit trail log for eZ Platform. This time around we'll install and use the Symfony MakerBundle to create the boilerplate code for our command:

janit@Turska:~/Sites/ezplatform (dev)*$ ./bin/console make:command

 Choose a command name (e.g. app:agreeable-pizza):
 > app:migrate-image-to-asset

 created: src/Command/MigrateImageToAssetCommand.php

  Success! 

 Next: open your new command class and customize it!
 Find the documentation at https://symfony.com/doc/current/console.html

Since this is a repeatable task we'll add arguments so that our command is reusable:

  • Content type identifier
  • Source image field identifier
  • Target image asset field identifier
  • Target location where image objects will be placed

In the command classes setting the options is done in the configure method as follows:

protected function configure(): void
{
    $this
        ->setDescription('Copies image field type contents to an image asset field')
        ->addArgument('type_identifier', InputArgument::REQUIRED, 'Identifier of content type whose to data is to be modified')
        ->addArgument('source_field', InputArgument::REQUIRED, 'Source field identifier')
        ->addArgument('target_field', InputArgument::REQUIRED, 'Target field identifier')
        ->addArgument('target_location', InputArgument::REQUIRED, 'Target location id where image objects should be created');
}

In the main execute method we'll add the main logic to log in as an admin user, find and update all the content objects:

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);
    $contentTypeIdentifier = $input->getArgument('type_identifier');
    $sourceFieldIdentifier = $input->getArgument('source_field');
    $targetFieldIdentifier = $input->getArgument('target_field');
    $imageTargetLocationId = $input->getArgument('target_location');

    $this->permissionResolver->setCurrentUserReference(
        $this->userService->loadUser(self::IMPORT_USER)
    );

    $searchResults = $this->loadContentObjects($contentTypeIdentifier);

    foreach ($searchResults as $searchHit) {
        /** @var ContentObject $contentObject */
        $contentObject = $searchHit->valueObject;

        try {
            $this->updateContentObject($contentObject, $sourceFieldIdentifier, $targetFieldIdentifier, $imageTargetLocationId);
            $io->writeln('Updated ' . $contentObject->contentInfo->name . ' (' . $contentObject->id . ')');
        } catch (RepositoryException $e) {
            $io->error(sprintf(
                'Unable to update %s (%d): %s',
                $contentObject->contentInfo->name,
                $contentObject->contentInfo->id,
                $e->getMessage()
            ));

            return self::MIGRATION_ERROR;
        }
    }

    return self::MIGRATION_SUCCESS;
}

The main method above uses some helper methods. First up is the loadContentObjects method that runs a search using the search service with a single criterion, the Content Type Identifier:

private function loadContentObjects($contentTypeIdentifier): SearchResult
{
    $query = new Query();
    $query->query = new Query\Criterion\ContentTypeIdentifier($contentTypeIdentifier);
    $query->limit = 1000;

    return $this->searchService->findContent($query);
}

This each content object from the search result set is the feed to the updateContentObject method that handles the creation and linking of image objects using the eZ Platform PHP API:

private function updateContentObject(ContentObject $contentObject, $sourceFieldIdentifier, $targetFieldIdentifier, $imageTargetLocationId): void
{
    $imageObjectRemoteId = $this->getImageRemoteId($contentObject, $sourceFieldIdentifier);

    $imageFieldValue = $contentObject->getFieldValue($sourceFieldIdentifier);
    $imageObject = $this->createOrUpdateImage($imageObjectRemoteId, $imageTargetLocationId, $imageFieldValue);

    $contentDraft = $this->contentService->createContentDraft($contentObject->contentInfo);

    $contentUpdateStruct = $this->contentService->newContentUpdateStruct();
    $contentUpdateStruct->initialLanguageCode = self::IMAGE_LANGUAGE;

    $contentUpdateStruct->setField($targetFieldIdentifier, $imageObject->id);

    $draft = $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct);
    $content = $this->contentService->publishVersion($draft->versionInfo);
}

The above code snippet calls another helper method, createOrUpdateImage, which creates or updates a content ID based on the the Remote ID metadata field explicitly defined above. Similar to the main object update method it uses the services provided by the public PHP API:

private function createOrUpdateImage(string $remoteId, int $parentLocationId, ImageFieldValue $imageFieldValue): ContentObject
{
    $contentType = $this->contentTypeService->loadContentTypeByIdentifier(self::IMAGE_CONTENT_TYPE);

    $imageName = $imageFieldValue->fileName;
    $imagePath = getcwd() . '/public' . $imageFieldValue->uri;

    try {
        $contentObject = $this->contentService->loadContentByRemoteId($remoteId, [self::IMAGE_LANGUAGE]);

        $contentDraft = $this->contentService->createContentDraft($contentObject->contentInfo);

        $contentUpdateStruct = $this->contentService->newContentUpdateStruct();
        $contentUpdateStruct->initialLanguageCode = self::IMAGE_LANGUAGE;

        $contentUpdateStruct->setField('name', $imageName);
        $contentUpdateStruct->setField('image', $imagePath);

        $draft = $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct);
        $content = $this->contentService->publishVersion($draft->versionInfo);
    } catch (NotFoundException $e) {
        // Not found, create new object

        $contentCreateStruct = $this->contentService->newContentCreateStruct($contentType, self::IMAGE_LANGUAGE);
        $contentCreateStruct->remoteId = $remoteId;

        $contentCreateStruct->setField('name', $imageName);
        $contentCreateStruct->setField('image', $imagePath);

        $locationCreateStruct = $this->locationService->newLocationCreateStruct($parentLocationId);
        $draft = $this->contentService->createContent($contentCreateStruct, [$locationCreateStruct]);
        $content = $this->contentService->publishVersion($draft->versionInfo);
    }

    return $content;
}

The full source code is on available on GitHub: github.com/janit/ezplatform-migrate-image-asset

With the code in place you can execute the data conversion using a command with arguments:

./bin/console app:migrate-image-to-asset blog_post image image_asset 9372

Once complete the script will have copied the content from the field to linked image asset. You will still need to perform the steps mentioned above to take this field into use in your code and clean up the image field. Each time you run the script it will refresh the image asset content, so you can run this just before removing the original image field to be sure you don't lose updates.

Conclusion

Established implementations tend to stick to the original field types that were originally used. With this command you can make moving data from an image field to an image asset field faster than manual work. This lowers the threshold for such migrations in existing projects.

Developers tend to focus on upgrading and refactoring code. And it is true that an unmaintained technical foundation will lead to software rot. But what is often overlooked is the maintenance of the data model. Adding new fields as requirement is a common task in continuous development.

But removing obsolete fields or switching field types is much more rare. This will degrade administration experience. This is similar to dead code, that is no longer being used but easily sticks around for years "just to be sure". Both outdated data model elements and unused code should be purged mercilessly from time to time.

 

eZ Platform is now Ibexa DXP

Ibexa DXP was announced in October 2020. It replaces the eZ Platform brand name, but behind the scenes it is an evolution of the technology. Read the Ibexa DXP v3.2 announcement blog post to learn all about our new product family: Ibexa Content, Ibexa Experience and Ibexa Commerce

Introducing Ibexa DXP 3.2

Insights and News