Make Your Entities Sortable in EasyAdmin
Imagine that your EasyAdmin administration backend contains an entity (Sponsor
in our example) and that you want to give the user the possibility to choose the order in which these sponsors are displayed on the application frontend (maybe because alphabetical sorting is not relevant).
This article presents a simple implementation of this need, based on the Sortable Doctrine extension and EasyAdmin custom actions.
Section intitulée set-up-the-sortable-extensionSet up the Sortable extension
Thanks to the Doctrine Sortable extension, we will be able to store and update the position of our entity using $entity->setPosition($position)
, without worrying about updating the position of the other entities because the extension will manage it for us.
To use it, we must first require the StofDoctrineExtensionsBundle which will simplify the extension configuration.
composer require stof/doctrine-extensions-bundle
This generates a configuration file in which we need to declare the extension we want to enable.
# config/packages/stof_doctrine_extensions.yaml
stof_doctrine_extensions:
default_locale: en_US
+ orm:
+ default:
+ sortable: true
Section intitulée update-our-entityUpdate our entity
Now, we need to update our entity to add the property that will store its position. We can also add an index to this new property for performance purposes.
// src/Entity/Sponsor.php
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
#[ORM\Index(name: 'position_idx', columns: ['position'])]
class Sponsor
{
// ...
#[Gedmo\SortablePosition]
#[ORM\Column]
private int $position = 0;
// ...
public function getPosition(): int
{
return $this->position;
}
public function setPosition(int $position): void
{
$this->position = $position;
}
}
We can now generate the migration and execute it to update our database.
php bin/console make:migration
If the property is added to a table already containing data, we need to modify the migration to avoid the position being 0 for each row and have a default sort.
To do that, add the following two lines to the migration file :
// migrations/Version20230525123934.php
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE sponsor ADD position INT NOT NULL');
+ $this->addSql('SET @current = -1');
+ $this->addSql('UPDATE sponsor SET position = (@current := @current + 1)');
$this->addSql('CREATE INDEX position_idx ON sponsor (position)');
}
If the property is added to an empty table, the migration can be executed directly. The Sortable extension takes care of incrementing the position value at each persistence.
php bin/console doctrine:migrations:migrate
Section intitulée configure-the-crud-controllerConfigure the CRUD controller
Section intitulée set-the-default-sortingSet the default sorting
We can define the default sorting by using the new $position
property. I also suggest displaying the actions inline to get the same rendering we saw in the screenshot of the introduction but this is optional.
// src/Controller/Admin/SponsorCrudController.php
public function configureCrud(Crud $crud): Crud
{
return $crud
->setDefaultSort(['position' => 'ASC'])
->showEntityActionsInlined();
}
Section intitulée create-the-custom-actionsCreate the custom actions
We are going to create four actions that will enable the following changes to the entity’s position: move the entity up one position, move it down one position, move it up to the first position, and move it down to the last position.
To create an action, we use the Action::new()
method of EasyAdmin. It takes a name, a label, and a Font Awesome icon as arguments.
// src/Controller/Admin/SponsorCrudController.php
public function configureActions(Actions $actions): Actions
{
$moveUp = Action::new('moveUp', false, 'fa fa-sort-up');
return $actions
->add(Crud::PAGE_INDEX, $moveUp);
}
Here, we use false
as a label to only display the icon. However, I suggest setting the HTML title attribute like this to improve the user experience:
$moveUp = Action::new('moveUp', false, 'fa fa-sort-up')
->setHtmlAttributes(['title' => 'Move up']);
To have a valid action, we need to define which method is executed when clicking on it. We are going to use a CrudAction
to take advantage of the AdminContext
which allows us to easily retrieve the object concerned by the modification. However, as CrudActions
do not accept parameters, we need to create one for each action so we will move the logic into another method to avoid duplicating code.
Let’s create in our controller the following methods and an enumeration for the directions:
// src/Controller/Admin/SponsorCrudController.php
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use Symfony\Component\HttpFoundation\Response;
enum Direction
{
case Top;
case Up;
case Down;
case Bottom;
}
class SponsorCrudController extends AbstractCrudController
{
// …
public function moveUp(AdminContext $context): Response
{
return $this->move($context, Direction::Up);
}
private function move(AdminContext $context, Direction $direction): Response
{
$object = $context->getEntity()->getInstance();
$newPosition = match($direction) {
Direction::Up => $object->getPosition() - 1,
};
$object->setPosition($newPosition);
$this->em->flush();
$this->addFlash('success', 'The element has been successfully moved.');
return $this->redirect($context->getRequest()->headers->get('referer'));
}
}
In the move()
method, we retrieve the entity instance thanks to the AdminContext
and update its position depending on the direction chosen (i.e. the action on which the user clicked). For the time being, we only manage the up
direction. We will see later how to manage the others. To be able to flush our change, we also need to inject the EntityManager
service into the constructor:
// src/Controller/Admin/SponsorCrudController.php
public function __construct(
private readonly EntityManagerInterface $em,
) {
}
Finally, we redirect the user to the original page thanks to the referrer and add a flash message to inform him about the modification.
Now that our CrudAction
is defined, we can link it to our action:
// src/Controller/Admin/SponsorCrudController.php
public function configureActions(Actions $actions): Actions
{
$moveUp = Action::new('moveUp', false, 'fa fa-sort-up')
->setHtmlAttributes(['title' => 'Move up'])
->linkToCrudAction('moveUp');
return $actions
->add(Crud::PAGE_INDEX, $moveUp);
}
This gives us the following result in our administration interface:
To improve this, we are going to remove the possibility of moving up the entity that is in first position thanks to the displayIf()
method of our Action
.
// src/Controller/Admin/SponsorCrudController.php
public function configureActions(Actions $actions): Actions
{
$moveUp = Action::new('moveUp', false, 'fa fa-sort-up')
->setHtmlAttributes(['title' => 'Move up'])
->linkToCrudAction('moveUp')
->displayIf(fn ($entity) => $entity->getPosition() > 0);
return $actions
->add(Crud::PAGE_INDEX, $moveUp);
}
The first element can no longer be moved. But how to do the same for the last element and the moveDown
action?
For the last element, we need to know the total number of elements. We can retrieve this information through the SponsorRepository
, injected into the constructor like this:
// src/Controller/Admin/SponsorCrudController.php
public function __construct(
private readonly EntityManagerInterface $em,
private readonly SponsorRepository $sponsorRepository,
) {
}
We can now use the repository to retrieve the number of elements and create all our actions:
// src/Controller/Admin/SponsorCrudController.php
public function configureActions(Actions $actions): Actions
{
$entityCount = $this->sponsorRepository->count([]);
$moveTop = Action::new('moveTop', false, 'fa fa-arrow-up')
->setHtmlAttributes(['title' => 'Move to top'])
->linkToCrudAction('moveTop')
->displayIf(fn ($entity) => $entity->getPosition() > 0);
$moveUp = Action::new('moveUp', false, 'fa fa-sort-up')
->setHtmlAttributes(['title' => 'Move up'])
->linkToCrudAction('moveUp')
->displayIf(fn ($entity) => $entity->getPosition() > 0);
$moveDown = Action::new('moveDown', false, 'fa fa-sort-down')
->setHtmlAttributes(['title' => 'Move down'])
->linkToCrudAction('moveDown')
->displayIf(fn ($entity) => $entity->getPosition() < $entityCount - 1);
$moveBottom = Action::new('moveBottom', false, 'fa fa-arrow-down')
->setHtmlAttributes(['title' => 'Move to bottom'])
->linkToCrudAction('moveBottom')
->displayIf(fn ($entity) => $entity->getPosition() < $entityCount - 1);
return $actions
->add(Crud::PAGE_INDEX, $moveBottom)
->add(Crud::PAGE_INDEX, $moveDown)
->add(Crud::PAGE_INDEX, $moveUp)
->add(Crud::PAGE_INDEX, $moveTop);
}
public function moveTop(AdminContext $context): Response
{
return $this->move($context, Direction::Top);
}
public function moveUp(AdminContext $context): Response
{
return $this->move($context, Direction::Up);
}
public function moveDown(AdminContext $context): Response
{
return $this->move($context, Direction::Down);
}
public function moveBottom(AdminContext $context): Response
{
return $this->move($context, Direction::Bottom);
}
private function move(AdminContext $context, Direction $direction): Response
{
$object = $context->getEntity()->getInstance();
$newPosition = match($direction) {
Direction::Top => 0,
Direction::Up => $object->getPosition() - 1,
Direction::Down => $object->getPosition() + 1,
Direction::Bottom => -1,
};
$object->setPosition($newPosition);
$this->em->flush();
$this->addFlash('success', 'The element has been successfully moved.');
return $this->redirect($context->getRequest()->headers->get('referer'));
}
Section intitulée update-the-sql-queryUpdate the SQL query
Now that our entity has a user-modifiable position, all we have to do is update our query to return the results in the order chosen by the user:
// src/Repository/SponsorRepository.php
public function findSponsors(): array
{
$qb = $this->createQueryBuilder('sponsor')
// … any other query conditions you need
->orderBy('sponsor.position', 'ASC');
return $qb->getQuery()->execute();
}
And retrieve them in the controller:
// src/Controller/SponsorController.php
#[Route('/sponsors', name: 'sponsors')]
public function list(SponsorRepository $sponsorRepository): Response
{
$sponsors = $sponsorRepository->findSponsors();
return $this->render('sponsor/list.html.twig', [
'sponsors' => $sponsors
]);
}
Commentaires et discussions
Nos formations sur ce sujet
Notre expertise est aussi disponible sous forme de formations professionnelles !
Symfony avancée
Découvrez les fonctionnalités et concepts avancés de Symfony
Ces clients ont profité de notre expertise
La refonte de la plateforme odealarose.com par JoliCode a représenté une transformation complète de tous les aspects de l’application. En charge de l’aspect technique, nous avons collaboré avec Digital Ping Pong (société cofondée par JoliCode), qui a eu en charge à la conception en revisitant entièrement le parcours client et l’esthétique de la plateforme…
Après avoir monté une nouvelle équipe de développement, nous avons procédé à la migration de toute l’infrastructure technique sur une nouvelle architecture fortement dynamique à base de Symfony2, RabbitMQ, Elasticsearch et Chef. Les gains en performance, en stabilité et en capacité de développement permettent à l’entreprise d’engager de nouveaux marchés…
Nous avons entrepris une refonte complète du site, initialement développé sur Drupal, dans le but de le consolider et de jeter les bases d’un avenir solide en adoptant Symfony. La plateforme est hautement sophistiquée et propose une pléthore de fonctionnalités, telles que la gestion des abonnements avec Stripe et Paypal, une API pour l’application…