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.
| Key | Accepted |
|---|---|
import, report_2026, invoice.42 | Yes |
'' (empty) | No |
user-42, ../etc, a/b, a b | No: 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).
lock() and unlock() short.5. Releasing a lock
Three ways, from the most explicit to the safety net:
$locker->unlock('import')releases it now.- When the
Lockerobject is destroyed, every lock it still holds is released. $locker->unlockIfKill()also releases them when the script ends, after anexit()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.
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
| Situation | Result |
|---|---|
Empty key, or a character other than letters, digits, _, . | InvalidArgumentException from lock() |
unlock() of a key this Locker does not hold | InvalidArgumentException |
| Key held by another process, non-blocking | lock() returns false |
FlockHandler cannot open the lock file | lock() returns false |
Errors while releasing locks at destruction or shutdown are written to the PHP error log, never thrown.