Documentation

Two classes and one interface. Locker is what you call; a handler does the actual locking.

1. Install

$ composer require michel/lock

PHP 7.4 or later. No extension, no other package.

2. Quick start

use Michel\Lock\Locker;
use Michel\Lock\Handler\FlockHandler;

$locker = new Locker(new FlockHandler());

if (!$locker->lock('nightly_import')) {
    echo "Already running.\n";
    exit(0);
}

// only one process at a time runs this
runImport();

$locker->unlock('nightly_import');

Run this script twice at the same time: the second one prints Already running. and stops.

3. Keys

A key names what you protect. Every process that locks the same key, with the same kind of handler, waits for the others.

KeyAccepted
import, report_2026, invoice.42Yes
'' (empty)No
user-42, ../etc, a/b, a bNo: only letters, digits, _ and .

To lock on any string (an email, a URL), hash it first: 'mail.' . md5($email).

4. Waiting or not

$locker->lock('import');         // non-blocking: false at once if it is taken
$locker->lock('import', true);   // blocking: waits until it is free, then true

Use non-blocking when a second run is useless (cron jobs, scheduled reports). Use blocking when every run must happen, one after the other (counters, file writes).

A blocking lock has no timeout. If the holder never releases it, the waiting process waits forever. Keep the code between lock() and unlock() short.

5. Releasing a lock

Three ways, from the most explicit to the safety net:

  1. $locker->unlock('import') releases it now.
  2. When the Locker object is destroyed, every lock it still holds is released.
  3. $locker->unlockIfKill() also releases them when the script ends, after an exit() or a fatal error.
$locker = (new Locker(new FlockHandler()))->unlockIfKill();

If the process itself is killed, no PHP code runs; with FlockHandler, the operating system releases the file lock of the dead process anyway.

A Locker only releases the keys it locked: unlock() on another key throws an exception.

6. FlockHandler

The default handler. Each key is a file, locked with flock():

$handler = new FlockHandler();                    // sys_get_temp_dir()
$handler = new FlockHandler('/var/lock/myapp');   // your own folder

The key import is the file <folder>/michel_lock_import.lock. The file stays after unlock(): only the lock on it is released. The folder must exist and be writable.

One machine only. A file lock is seen by the processes of the machine that holds the file. Keep the folder on a local disk: locks on network file systems (NFS, SMB) are not reliable. For several servers, write a handler on a shared backend.

7. Your own handler

Implement LockHandlerInterface: take the lock (waiting or not), release it.

interface LockHandlerInterface
{
    public function lock(string $key, bool $wait = false): bool;
    public function unlock(string $key): void;
}

For example, a lock shared by every server using the same MariaDB or MySQL database:

use Michel\Lock\LockHandlerInterface;

final class MysqlLockHandler implements LockHandlerInterface
{
    private \PDO $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function lock(string $key, bool $wait = false): bool
    {
        $statement = $this->pdo->prepare('SELECT GET_LOCK(?, ?)');
        $statement->execute([$key, $wait ? -1 : 0]);   // -1: wait without limit
        return (int) $statement->fetchColumn() === 1;
    }

    public function unlock(string $key): void
    {
        $this->pdo->prepare('SELECT RELEASE_LOCK(?)')->execute([$key]);
    }
}

$locker = new Locker(new MysqlLockHandler($pdo));

Locker checks the keys and tracks what it holds; your handler only talks to its backend.

8. Errors

SituationResult
Empty key, or a character other than letters, digits, _, .InvalidArgumentException from lock()
unlock() of a key this Locker does not holdInvalidArgumentException
Key held by another process, non-blockinglock() returns false
FlockHandler cannot open the lock filelock() returns false

Errors while releasing locks at destruction or shutdown are written to the PHP error log, never thrown.