Sylius: How to add position to product images

January 5th, 2022

Sylius Swan

I've recently been working on migrating a Magento 1 store to Sylius, an open source e-commerce platform based on the Symfony framework.

Whilst Sylius contains a lot of features, it doesn't come with functionality to change the ordering of images in the admin area. This is something I had to add manually.

Here's how to add a position to the product images tab.

Migration

First step is to add the position column to the product_images database table.

<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20220105112323 extends AbstractMigration
{
    public function getDescription() : string
    {
        return 'Adds a position field to product_images table';
    }

    public function up(Schema $schema) : void
    {

        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('ALTER TABLE sylius_product_image ADD COLUMN position INT UNSIGNED DEFAULT 1');
    }

    public function down(Schema $schema) : void
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('ALTER TABLE sylius_product_image DROP COLUMN position');
    }
}

Extend the form

Next we should extend the form type used in the admin area. To find the form type run the following in your terminal:

> php bin/console debug:container | grep product.image | grep form

sylius.form.type.product_image                   Sylius\Bundle\CoreBundle\Form\Type\Product\ProductImageType 

To find out which class you need to extend, use the console:

> php bin/console debug:container sylius.form.type.product_image

Information for Service "sylius.form.type.product_image"
========================================================

 ---------------- ------------------------------------------------------------- 
  Option           Value                                                        
 ---------------- ------------------------------------------------------------- 
  Service ID       sylius.form.type.product_image                               
  Class            Sylius\Bundle\CoreBundle\Form\Type\Product\ProductImageType  
  Tags             form.type                                                    
  Public           yes                                                          
  Synthetic        no                                                           
  Lazy             no                                                           
  Shared           yes                                                          
  Abstract         no                                                           
  Autowired        no                                                           
  Autoconfigured   no                                                           
 ---------------- ------------------------------------------------------------- 

Great, now we know that we need to extend the Sylius\Bundle\CoreBundle\Form\Type\Product\ProductImageType class. Let's do that:

Extend the class

<?php
declare(strict_types=1);

namespace App\Form\Extension;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Sylius\Bundle\CoreBundle\Form\Type\Product\ProductImageType;


final class ProductImageFormExtension extends AbstractTypeExtension
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('position', NumberType::class, [
                'required' => false,
                'label' => 'Position',
            ]);
    }

    public static function getExtendedTypes(): iterable
    {
        return [ProductImageType::class];
    }
}

Add the extended form to Sylius

Now that we have overridden the form class we need to let Sylius know about it. Open your config/services.yaml file and add the following in the services node:

    app.form.extension.type.product_image:
        class: App\Form\Extension\ProductImageFormExtension
        tags:
            - { name: form.type_extension, extended_type:  Sylius\Bundle\CoreBundle\Form\Type\Product\ProductImageType }

Looking good - go to a product in the admin area, and click on the 'Media' tab. What? No position field? Thats because we haven't told the template to show the position field yet.

Add position field to the template

Copy the image template from the vendor dir to your local theme folder:

cp vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/Resources/views/Form/imagesTheme.html.twig templates/bundles/SyliusAdminBundle/Form/

Then edit the file and add the position:

# templates/bundles/SyliusAdminBundle/Form/imagesTheme.html.twig
{% extends '@SyliusUi/Form/imagesTheme.html.twig' %}

{% block sylius_product_image_widget %}

    {{ block('sylius_image_widget') }}

    {# ADD THE POSITION FIELD HERE ↓ #}
    {{  form_row(form.position) }}

    {% apply spaceless %}
        {% if product.id is not null and 0 != product.variants|length and not product.simple %}
            <br/>
            {{ form_row(form.productVariants, {'remote_url': path('sylius_admin_ajax_product_variants_by_phrase', {'productCode': product.code}), 'remote_criteria_type': 'contains', 'remote_criteria_name': 'phrase', 'load_edit_url': path('sylius_admin_ajax_product_variants_by_codes', {'productCode': product.code})}) }}
        {% endif %}
    {% endapply %}
{% endblock %}

Test it works in the admin

Clear your caches and reload the admin product page. You should see the position field underneath the image 'type' field.

Change the value in a couple of fields and hit the 'Save Changes' button. If all went well it should have added your new values to the database. Reload the product_images table and order by the position field (desc).

Add sorting to Product Entity

To get the product images sorting by position you can add the following method to your Product Entity class:

<?php

namespace namespace App\Entity\Product;

class Product extends BaseProduct implements ProductInterface {

    // ...

    public function getImages(): Collection
    {
        /** @var PersistentCollection $collection */
        $collection = $this->images;

        $values = $collection->getValues();
        uasort($values, function($a, $b){
            return $a->getPosition() < $b->getPosition() ? -1 : 1;
        });

        $collection->clear();

        foreach ($values as $key => $value) {
            $collection->set($key, $value);
        }
        return $collection;
    }
    
    // ...
 }

Note: It has been suggested that adding a Doctrine OrderBy annotation to the Product Entity images property would handle the sorting (see below). I tried this and it didn't work.

If anyone knows a better way to do this please let me know in the comments section.

<?php

namespace namespace App\Entity\Product;

class Product extends BaseProduct implements ProductInterface {
    /**
     * @ORM\OrderBy({"position" = "ASC"})
     */
     protected $images; 

    // ...
}