869 mots
4 minutes
Maîtrisez le verrouillage de traitements avec Symfony Lock

Introduction à Symfony Lock : Comment verrouiller vos traitements#

Dans certains cas, il peut être utile de verrouiller des traitements ou des objets. Par exemple, si vous êtes en train de modifier un article de blog et que vous ne voulez pas que quelqu’un le modifie en même temps que vous, vous pouvez verrouiller l’objet avant de le modifier.

Dans cet article, nous allons utiliser le composant Lock de Symfony (https://symfony.com/doc/current/components/lock.html) pour effectuer cela. Vous verrez que c’est très simple à mettre en œuvre !

Création du projet et installation des dépendances#

Commençons par créer notre projet et installer toutes les dépendances nécessaires :

symfony new lock --webapp
composer req --dev symfony/maker-bundle
composer req symfony/lock
composer req orm ormfixtures
composer req --dev fakerphp/faker

Maintenant que nous avons tout ce dont nous avons besoin, nous pouvons créer notre entité. Comme d’habitude, nous allons faire simple :

<?php

namespace App\Entity;

use App\Repository\PostRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $content = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(string $content): static
    {
        $this->content = $content;

        return $this;
    }
}

Ensuite, nous créons des fixtures de test :

<?php

namespace App\DataFixtures;

use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use App\Entity\Post;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {

        $faker = \Faker\Factory::create();

        for ($i = 0; $i < 10; $i++) {
            $post = new Post();
            $post->setTitle($faker->sentence);
            $post->setContent($faker->paragraph);

            $manager->persist($post);
        }

        $manager->flush();
    }
}

Et enfin, nous créons tout cela dans la base de données :

bin/console doctrine:database:create
bin/console make:migration
bin/console doctrine:migration:migrate
bin/console doctrine:fixtures:load

Mise en place du verrouillage#

Le premier cas que nous allons mettre en place sera un verrouillage simple dans une commande. Cette dernière ne pourra pas être exécutée si elle est déjà en cours d’exécution. Pour cela, nous allons utiliser la classe LockFactory.

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;

#[AsCommand(
    name: 'app:lock',
    description: 'Ajout d\'un verrou pour empêcher l\'exécution simultanée d\'un traitement',
)]
class LockCommand extends Command
{
    public function __construct(private LockFactory $lockFactory)
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        
        $lock = $this->lockFactory->createLock('my-lock');

        if ($lock->acquire()) {
            //on met une pause de 30 secondes
            sleep(30);

            $lock->release();
            $io->success('Traitement terminé.');
        } else {
            $io->error('Traitement déjà en cours.');
        }

        return Command::SUCCESS;
    }
}

Nous créons d’abord un verrou avec createLock en paramètre le nom du verrou. acquire permet de savoir si le verrou est déjà en cours, et release permet de relâcher le verrou.

Pour tester, il suffit de lancer votre commande dans deux fenêtres de terminal différentes.

app-lock.png

Note : Par défaut, cette bibliothèque utilise le store flock. Vous pouvez vérifier cela dans le fichier .env :

###> symfony/lock ###
# Choose one of the stores below
# postgresql+advisory://db_user:db_password@localhost/db_name
LOCK_DSN=flock
###< symfony/lock ###

En consultant la documentation, vous verrez qu’il existe plusieurs types de store pour différents cas d’utilisation.

store.png

Le deuxième cas que nous allons mettre en place sera presque identique au premier. Cependant, au lieu que le second script s’arrête, il attendra que le verrou se libère.

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;

#[AsCommand(
    name: 'app:lock-block',
    description: 'Ajout d\'un verrou pour empêcher l\'exécution simultanée d\'un traitement',
)]
class LockBlockCommand extends Command
{
    public function __construct(private LockFactory $lockFactory)
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $lock = $this->lockFactory->createLock('my-lock');

        $lock->acquire(true);

        sleep(30);

        $lock->release();

        $io->success('Traitement terminé.');

        return Command::SUCCESS;
    }
}

app-lock-block.png

Pour le troisième cas, nous allons utiliser notre entité créée plus haut. Nous allons bloquer la modification tant que le verrou n’est pas libéré.

<?php

namespace App\Command;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory;
use App\Entity\Post;

#[AsCommand(
    name: 'app:lock-object',
    description: 'Ajout d\'un verrou pour empêcher l\'exécution simultanée d\'un traitement',
)]
class LockObjectCommand extends Command
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private LockFactory $lockFactory)
    {
        parent::__construct();
    }

    protected function configure(): void
    {
        //on recupere l'argument, id de l'objet
        $this
            ->addArgument('id', InputArgument::REQUIRED, 'Id de l\'objet');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $idObject = $input->getArgument('id');

        //On recupere l'objet id 1
        $post = $this->entityManager->getRepository(Post::class)->find($idObject);
        $key = new Key('lock_' . $post->getId());
        
        $lock = $this->lockFactory->createLock($key);

        if ($lock->acquire()) {
            //on met une pause de 30 secondes
            sleep(30);

            //Update de l'objet
            $post->setTitle('Titre modifié');
            $this->entityManager->flush();

            $lock->release();
            $io->success('Traitement terminé.');
        } else {
            $io->error('Objet déjà en cours de traitement.');
        }

        return Command::SUCCESS;
    }
}

Pour ce cas, il suffit de passer un ID de Post en paramètre de cette commande. Une erreur sera déclenchée si on essaie de modifier le même post.

app-lock-object.png

En conclusion, le verrouillage de traitements ou d’objets est une fonctionnalité utile dans de nombreuses situations de développement web. Avec le composant Symfony Lock, sa mise en œuvre est simple et facile à comprendre. N’hésitez pas à explorer cette fonctionnalité et à l’adapter à vos besoins spécifiques.

Pour terminer, je vous souhaite à toutes et à tous une excellente année 2025, pleine de lignes de code, de points d’arrêt et de résolution de bugs (mais pas trop tout de même :) !