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
Groupama Épargne Salariale digitalise son expérience client en leur permettant d’effectuer leurs versements d’épargne salariale en ligne. L’application offre aux entreprises une interface web claire et dynamique, composé d’un tunnel de versement complet : import des salariés via fichier Excel, rappel des contrats souscrits et des plans disponibles, …
Dans le cadre du renouveau de sa stratégie digitale, Orpi France a fait appel à JoliCode afin de diriger la refonte du site Web orpi.com et l’intégration de nombreux nouveaux services. Pour effectuer cette migration, nous nous sommes appuyés sur une architecture en microservices à l’aide de PHP, Symfony, RabbitMQ, Elasticsearch et Docker.
Ouibus a pour ambition de devenir la référence du transport en bus longue distance. Dans cette optique, les enjeux à venir de la compagnie sont nombreux (vente multi-produit, agrandissement du réseau, diminution du time-to-market, amélioration de l’expérience et de la satisfaction client) et ont des conséquences sur la structuration de la nouvelle…