symfony/lock
Symfony Lock component provides a unified API to create and manage locks, ensuring exclusive access to shared resources. Supports multiple backends (e.g., filesystem, Redis, PDO) to prevent race conditions in concurrent PHP apps.
Install the package:
composer require symfony/lock
For Laravel, use the symfony/lock facade or bind it in config/app.php:
'aliases' => [
'Lock' => Symfony\Component\Lock\LockFactory::class,
],
Configure a store (e.g., Redis):
use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\LockFactory;
$store = new RedisStore(new \Redis());
$factory = new LockFactory($store);
First use case: Protect a critical section in a job or command:
use Symfony\Component\Lock\LockFactory;
public function handle()
{
$lock = $this->lockFactory->createLock('inventory_update_' . $this->orderId, 30);
$acquired = $lock->acquire(true); // Block until acquired
if ($acquired) {
// Critical section: update inventory
Inventory::decrement($this->productId);
$lock->release(); // Release when done
}
}
Laravel integration:
Bind the factory in AppServiceProvider:
public function register()
{
$this->app->singleton(LockFactory::class, function ($app) {
$store = new RedisStore($app['redis']);
return new LockFactory($store);
});
}
Use the Lock facade in controllers/jobs:
use Illuminate\Support\Facades\Lock;
Lock::acquire('critical_section', 10); // Non-blocking
public function handle()
{
$lock = Lock::createLock('job_' . $this->data['id'], 60);
if ($lock->acquire()) {
try {
$this->processData();
} finally {
$lock->release();
}
}
}
dispatchSync() with locks for fire-and-forget jobs requiring idempotency.DB::transaction(function () use ($lock) {
$lock->acquire(); // Acquire before DB ops
// Transaction logic
$lock->release();
});
PdoStore with LOCK TABLE for heavy DB workloads (see Gotchas).$store = new RedisStore($redisClient, 'locks_');
$factory = new LockFactory($store);
30 seconds for short jobs).public function handle()
{
$lock = Lock::createLock('command_' . $this->command, 3600);
if (!$lock->acquire()) {
throw new \RuntimeException('Command already running.');
}
}
Service Container: Bind stores dynamically:
$this->app->bind(RedisStore::class, function ($app) {
return new RedisStore($app['redis'], 'laravel_locks_');
});
Lock Factory: Extend for custom logic:
class AppLockFactory extends LockFactory
{
public function createNamedLock(string $name, int $ttl = 30): LockInterface
{
return $this->createLock("app_{$name}", $ttl);
}
}
Fallback Chain: Combine stores for resilience:
$primary = new RedisStore($redis);
$fallback = new FlockStore('/tmp/locks');
$factory = new LockFactory(new ChainedStore([$primary, $fallback]));
Store-Specific Keys: Prefix keys to avoid collisions:
$store = new RedisStore($redis, 'myapp_');
public function handle(OrderPlaced $event)
{
$lock = Lock::createLock('order_' . $event->order->id, 120);
if ($lock->acquire()) {
// Process once
$lock->release();
}
}
Lock Leaks
try-finally or context managers:
$lock = Lock::createLock('resource', 60);
try {
if ($lock->acquire()) {
// Work
}
} finally {
$lock->release(); // Always release
}
Lock::release() in finally blocks or try-catch.PostgreSQL Transaction Contention
PdoStore with LOCK TABLE can abort outer transactions (fixed in v8.0.9+).SELECT FOR UPDATE instead:
$store = new PdoStore($pdo, 'locks', 'SELECT * FROM locks WHERE name = :name FOR UPDATE');
Redis Cluster Quirks
evalSha (fixed in v7.2.6+).RedisStore with useEval: false:
$store = new RedisStore($redis, null, null, false);
Key Normalization
LockKeyNormalizer:
$normalizer = new LockKeyNormalizer();
$key = $normalizer->normalize($customKey);
Windows File Locks
FlockStore may fail on network drives.RedisStore or PdoStore for cross-platform reliability.Lock Timeouts
$lock = Lock::createLock('resource', 10); // 10-second TTL
if (!$lock->acquire(true, 5)) { // Wait 5s
throw new \RuntimeException('Lock acquisition timed out.');
}
Redis Errors
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379, 0, null, 5.0, 2000, \Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY);
$store = new RedisStore($redis);
PostgreSQL Locks
pg_locks:
SELECT * FROM pg_locks WHERE relation::regclass = 'locks';
Custom Stores
StoreInterface for new backends (e.g., DynamoDB):
class DynamoStore implements StoreInterface
{
public function acquire($name, $ttl, $block = false, $timeout = null): bool
{
// DynamoDB logic
}
// ... other methods
}
Lock Strategies
LockFactory for custom acquisition logic:
class RetryLockFactory extends LockFactory
{
public function createLock(string $name, int $ttl = 30): LockInterface
{
return new RetryLock($this->store, $name, $ttl, 3); // Retry 3 times
}
}
Event Listeners
LockAcquiredEvent):
$factory->addListener(new class implements LockEvents {
public function onLockAcquired(LockAcquiredEvent $event) {
Log::info("Lock acquired: {$event->getLock()->getName()}");
}
});
How can I help you explore Laravel packages today?