symfony/semaphore
The Symfony Semaphore Component manages semaphores, a mechanism that provides exclusive access to a shared resource. It is a standalone PHP library you can use in any project to coordinate concurrent processes and prevent race conditions.
Installation (pin the current security release):
composer require symfony/semaphore:^5.4.54
5.4.54 is the [SECURITY] patch release — do not pin below it. The component is framework-agnostic, so there is no Laravel auto-discovered service provider; bind the interface yourself in a service provider:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\Symfony\Component\Semaphore\SemaphoreInterface::class, function () {
return new \Symfony\Component\Semaphore\Semaphore(
new \Symfony\Component\Semaphore\Store\RedisStore(
\Illuminate\Support\Facades\Redis::connection('default')->client()
)
);
});
}
Basic Usage:
// Acquire a semaphore (blocking)
$semaphore = app(\Symfony\Component\Semaphore\SemaphoreInterface::class);
$semaphore->acquire('critical_section_key');
// Critical section (e.g. update shared resource)
try {
// Your logic here
} finally {
$semaphore->release(); // Always release!
}
Non-Blocking Check:
if ($semaphore->acquire('key', 0)) { // 0 = non-blocking
// Proceed if lock acquired
}
Prevent duplicate job execution:
use Symfony\Component\Semaphore\SemaphoreInterface;
class ProcessPaymentJob implements ShouldQueue
{
public function handle(SemaphoreInterface $semaphore)
{
$key = 'payment_' . $this->paymentId;
if (!$semaphore->acquire($key, 30)) { // 30-second timeout
throw new \RuntimeException("Payment already being processed");
}
try {
// Process payment
} finally {
$semaphore->release();
}
}
}
composer update symfony/semaphore --with-dependencies (the release also touches polyfill declarations).composer audit — confirm no remaining advisories for the component.symfony/polyfill-* packages install cleanly after the lockfile bump (see Gotchas).Exclusive Locking:
$semaphore->acquire('resource_key', 10); // Block for 10 seconds
// Critical section
$semaphore->release();
Shared Locking (Multiple Workers): Use Redis backend for distributed systems:
$semaphore = new Semaphore(new RedisStore($redisClient));
Context Managers (Laravel): Create a reusable trait for jobs:
trait UsesSemaphore
{
protected function acquireSemaphore(string $key, int $timeout = 10): bool
{
return app(SemaphoreInterface::class)->acquire($key, $timeout);
}
protected function withSemaphore(string $key, Closure $callback, int $timeout = 10)
{
$semaphore = app(SemaphoreInterface::class);
$semaphore->acquire($key, $timeout);
try {
return $callback();
} finally {
$semaphore->release();
}
}
}
Laravel Queues:
Combine with ShouldQueue to prevent overlapping jobs:
$semaphore->acquire('queue_job_' . $jobId, 60);
// Job logic
$semaphore->release();
API Rate Limiting:
$semaphore = app(SemaphoreInterface::class);
$key = 'api_rate_limit_' . $userId . '_' . now()->format('Y-m-d');
if (!$semaphore->acquire($key, 0)) {
return response()->json(['error' => 'Rate limit exceeded'], 429);
}
try {
// Process request
} finally {
$semaphore->release();
}
Cron Jobs:
$semaphore = app(SemaphoreInterface::class);
if ($semaphore->acquire('cron_daily_backup', 0)) {
// Run backup
$semaphore->release();
}
Database Transactions: Use semaphores for row-level locking when transactions aren’t sufficient:
$semaphore->acquire('user_' . $userId . '_profile_update');
try {
DB::beginTransaction();
// Update user profile
DB::commit();
} finally {
$semaphore->release();
}
Forgetting to Release:
Always use finally blocks or context managers to avoid deadlocks:
// ❌ Dangerous
if ($semaphore->acquire('key')) {
// Might throw exception
$semaphore->release(); // Skipped if exception occurs
}
// ✅ Safe
try {
$semaphore->acquire('key');
// Critical section
} finally {
$semaphore->release();
}
Filesystem Backend Limitations:
Timeouts:
30 seconds for queue jobs).Key Collisions:
user_{id}_action_{type}).'lock'—they cause contention.Redis Cluster/Sentinel: Ensure your Redis DSN supports clusters/sentinels:
$semaphore = new Semaphore(new RedisStore($redisClient));
Build the client from your Laravel database.redis config rather than hard-coding a DSN, so cluster/sentinel options carry over.
Version Drift After the Security Release:
^5.4.x range will happily resolve to a vulnerable patch if your lockfile predates 5.4.54. Enforce the floor:
"require": { "symfony/semaphore": "^5.4.54" }
composer.lock on a PHP 7.2 host may fail to install — run composer update symfony/semaphore symfony/polyfill-* --with-dependencies once, then redeploy.Stuck Locks: Check Redis keys or filesystem locks manually:
# Redis
redis-cli KEYS "*semaphore*"
# Filesystem (Linux)
lsof +D /path/to/locks
Deadlocks:
Use acquire(..., 0) (non-blocking) to check lock status before proceeding.
Verifying the Patch Level in Production:
composer show symfony/semaphore # expect >= 5.4.54
php artisan about # confirm nothing overrode the lockfile
Custom Backends:
Implement SemaphoreInterface for specialized storage (e.g., database):
class DatabaseSemaphore implements SemaphoreInterface
{
public function acquire(string $name, int $timeout = null): bool
{
// Custom logic (e.g., SELECT FOR UPDATE in PostgreSQL)
}
// ...
}
Laravel Binding:
Bind the interface once in AppServiceProvider (the component has no Laravel provider of its own):
public function register()
{
$this->app->singleton(SemaphoreInterface::class, function () {
return new Semaphore(new RedisStore(
Redis::connection('cache')->client()
));
});
}
Retry Logic:
Combine with Laravel’s retry helper for transient failures:
retry(3, function () use ($semaphore, $key) {
return $semaphore->acquire($key, 10);
}, function () {
// Retry logic
});
Redis Connection: Ensure your Laravel Redis config supports the semaphore’s needs:
'connections' => [
'semaphore' => [
'url' => env('REDIS_SEMAPHORE_URL'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
],
],
],
Filesystem Locks:
Set storage_path('framework/semaphore') as the lock directory in config/filesystems.php:
'disks' => [
'semaphore' => [
'driver' => 'local',
'root' => storage_path('framework/semaphore'),
],
],
PHP 7.2 / Polyfills:
5.4.54 adds explicit polyfill declarations. Nothing to configure, but symfony/polyfill-php* packages now appear as direct transitive requirements — keep them out of your replace/provide overrides.
user_123_profile instead of user_profile_123).batch_import_2023-10-01).How can I help you explore Laravel packages today?