829 words
4 minutes
Master the treatment locking with Symfony Lock

Introduction to Symfony Lock: How to Lock Your Processes#

In some cases, it can be useful to lock processes or objects. For example, if you are editing a blog post and you don’t want someone to modify it at the same time as you, you can lock the object before editing it.

In this article, we are going to use the Lock component of Symfony (https://symfony.com/doc/current/components/lock.html) to do this. You will see that it is very simple to implement!

Project Creation and Dependency Installation#

Let’s start by creating our project and installing all the necessary dependencies:

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

Now that we have everything we need, we can create our entity. As usual, we will keep things 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;
    }
}

Next, we create test fixtures:

<?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();
    }
}

And finally, we create all this in the database:

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

Lock Implementation#

The first case we will set up will be a simple lock in a command. This command cannot be run if it is already running. For this, we are going to use the LockFactory class.

<?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: 'Add a lock to prevent simultaneous execution of a process',
)]
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()) {
            //we put a 30 seconds pause
            sleep(30);

            $lock->release();
            $io->success('Process finished.');
        } else {
            $io->error('Process already in progress.');
        }

        return Command::SUCCESS;
    }
}

We first create a lock with createLock as the lock name parameter. acquire allows to know if the lock is already in progress, and release releases the lock.

To test, just run your command in two different terminal windows.

app-lock.png

Note: By default, this library uses the flock store. You can verify this in the .env file:

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

By consulting the documentation, you will see that there are several types of stores for different use cases.

store.png

The second case we will set up will be almost identical to the first. However, instead of the second script stopping, it will wait for the lock to be released.

<?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: 'Add a lock to prevent simultaneous execution of a process',
)]
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('Process finished.');

        return Command::SUCCESS;
    }
}

app-lock-block.png

For the third case, we are going to use our entity created above. We will block the modification as long as the lock is not released.

<?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: 'Add a lock to prevent simultaneous execution of a process',
)]
class LockObjectCommand extends Command
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private LockFactory $lockFactory)
    {
        parent::__construct();
    }

    protected function configure(): void
    {
        //we retrieve the argument, id of the object
        $this
            ->addArgument('id', InputArgument::REQUIRED, 'Id of the object');
    }

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

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

        //We retrieve the object id 1
        $post = $this->entityManager->getRepository(Post::class)->find($idObject);
        $key = new Key('lock_' . $post->getId());
        
        $lock = $this->lockFactory->createLock($key);

        if ($lock->acquire()) {
            //we put a 30 seconds pause
            sleep(30);

            //Update of the object
            $post->setTitle('Modified title');
            $this->entityManager->flush();

            $lock->release();
            $io->success('Process finished.');
        } else {
            $io->error('Object already in progress.');
        }

        return Command::SUCCESS;
    }
}

For this case, just pass a Post ID as a parameter of this command. An error will be triggered if we try to modify the same post.

app-lock-object.png

In conclusion, the locking of processes or objects is a useful feature in many web development situations. With the Symfony Lock component, its implementation is simple and easy to understand. Feel free to explore this feature and adapt it to your specific needs.

Finally, I wish you all a very happy 2025, full of code lines, breakpoints, and bug fixes (but not too much :)!