9.1 - How to convert en entity to Sortable


Basics

Update your composer configuration, you need to require Doctrine extensions.

composer require stof/doctrine-extensions-bundle

Configure the package

stof_doctrine_extensions:
    default_locale: '%kernel.default_locale%'
    orm:
        default:
        sortable: true

Configure your entity

Add new property in entity model

// src/Entity/Book.php

    #[ORM\Column(type: 'integer', options: ['default' => 0])]
    #[SortablePosition]
    private int $position = 0;

    public function getPosition(): int
    {
        return $this->position;
    }

    public function setPosition(int $position): void
    {
        $this->position = $position;
    }

Do not forget to generate migration for the new entity property.

$ bin/console doctrine:migration:diff

Bringing the sorting ability to backend grid

Add field to entity grid

# src/Grid/BookGrid.php

$gridBuilder
    ->orderBy('position', 'asc')
    ->addField(
        TwigField::create('position', 'backend/book/grid/field/position_with_buttons.html.twig')
            ->setLabel('sylius.ui.position')
            ->setPath('.')
            ->setSortable(true),
    )

Add new grid field template

<!-- template/backend/book/grid/field/position_with_buttons.html.twig -->
<div style="text-align: center;"><span class="ui circular label">{{ data.position }}</span></div>

<div class="ui vertical menu">
    <div class="item button app-book-move-up" data-url="{{ path('app_backend_ajax_book_move', { id: data.id }) }}" data-id="{{ data.id }}" data-position="{{ data.position }}">
        <i class="arrow up icon grey"></i>{{ 'sylius.ui.move_up'|trans }}
    </div>
    {% if data.position > 0 %}
    <div class="item button app-book-move-down" data-url="{{ path('app_backend_ajax_book_move', { id: data.id }) }}" data-id="{{ data.id }}" data-position="{{ data.position }}">
        <i class="arrow down icon grey"></i>{{ 'sylius.ui.move_down'|trans }}
    </div>
    {% endif %}
</div>

Create a form specifically for updating position entity property

// src/Form/Type/Book/BookPositionType.php

declare(strict_types=1);

namespace App\Form\Type\Book;

use App\Entity\Book\BookType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class BookPositionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('position', IntegerType::class, [
                'label' => 'sylius.ui.position',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Book::class,
            'csrf_protection' => false,
        ]);
    }

    public function getBlockPrefix(): string
    {
        return 'app_book_position';
    }
}

Build a new route to handle position in entity

#[SyliusRoute(
    name: 'app_backend_ajax_book_move',
    path: '/admin/ajax/book/{id}/move',
    controller: 'app.controller.book::updateAction',
    form: BookPositionType::class,
)]

Link everything with some Ajax

// assets/backend/js/app-move-book.js
import 'semantic-ui-css/components/api';
import $ from 'jquery';

$.fn.extend({
  bookMoveUp() {
    const element = this;

    element.api({
      method: 'PUT',
      on: 'click',
      beforeSend(settings) {
        /* eslint-disable-next-line no-param-reassign */
        settings.data = {
          position: $(this).data('position') - 1,
        };

        return settings;
      },
      onSuccess() {
        window.location.reload();
      },
    });
  },

  bookMoveDown() {
    const element = this;

    element.api({
      method: 'PUT',
      on: 'click',
      beforeSend(settings) {
        /* eslint-disable-next-line no-param-reassign */
        settings.data = {
          position: $(this).data('position') + 1,
        };

        return settings;
      },
      onSuccess() {
        window.location.reload();
      },
    });
  },
});

And add these lines to main javascript file app.js.

// assets/backend/js/main.js
import './app-move-book';
[...]
    $('.app-book-move-up').bookMoveUp();
    $('.app-book-move-down').bookMoveDown();

API Platform

If you're using api-pack, you might want to sort your entities with the new position field : add the order property.

#[ApiResource(
    normalizationContext: ['groups' => ['book:read']],
    order: ['position' => 'ASC'],
)]

Updating tests

  • Update your BookFactory class to add position in your fixtures.
  • Add functional tests according to the purpose of sorting in your app.
  • If you use ApiPlatform, update your previous tests results with the new entity sorting.