symfony/semaphore
Symfony Semaphore provides a mechanism for managing semaphores, letting concurrent processes coordinate and gain exclusive or weighted access to shared resources. It supports expiration and both System V and Redis-backed storage, making it easy to prevent race conditions in PHP applications.
Architecture fit
symfony/semaphore is a framework-agnostic Symfony component, not a Laravel package. There is no service provider, facade, config file, or published config — it is a library of ~4 classes (SemaphoreFactory, SemaphoreInterface, Semaphore, plus store adapters) that compiles into Laravel cleanly because Laravel already runs on Symfony primitives. That is both the appeal and the constraint: integration cost is a thin internal wrapper, not a package install.
The architectural question a TPM must force is counting semaphore vs. binary lock, because Laravel already ships Cache::lock() / Redis::funnel and most "do we need this?" requests are actually satisfied by what is already in the box. The defensible use cases are the ones where the answer is N, not 1:
RateLimiter answers "how many per minute," not "how many in flight."If the requirement is plain mutual exclusion, this package is redundant and should be rejected on that basis.
Integration feasibility
Composer-installable, no framework coupling, MIT-licensed, no transitive weight beyond the store layer (symfony/lock and a Redis client). It fits Laravel's container as a singleton with roughly ten lines of binding code. The real friction is version alignment: Laravel 10/11/12 pin symfony/* at ^6.4/^7.x while the tracked target here is v8.1.0-BETA3, which requires PHP >= 8.4. A pre-release on a brand-new major is not a production baseline for most Laravel estates. Feasible — but only on the stable 6.4/7.4 line, which means the 8.1 features (lock-backed store, SemaphoreKeyNormalizer) are out of reach.
Technical risk
symfony/symfony. A TPM must not read this as "no adoption" (false negative) nor as "battle-tested by proxy" without checking Packagist download stats and the monorepo's issue history.-BETA3 with no documented GA exit criteria is a schedule risk. v8.1.0-BETA3's changelog entry is "various fixes and hardenings," which is a warning that the surface was still moving.v5.4.54 is tagged [SECURITY] yet its only changelog line is a PHP 7.2 polyfill declaration — a legacy-hygiene patch, not a behaviour change. Two risks: (a) teams may over-react to the [SECURITY] tag, and (b) teams on 5.4 are on an unsupported branch and should treat this as a migration forcing function, not a routine bump. Confirm the advisory ID and patched-version list; do not assume the 5.4 patch covers the line you run.acquire() in the request path consumes a PHP-FPM/Octane worker while waiting. Under load this converts a contention problem into an availability problem.Key questions
symfony/* constraints without a composer update cascade?Cache::lock() already solves?-BETA component into production, and who signs off if so? What is the exit criteria to stable 8.1?finally blocks.Stack fit
Target Laravel 10+ on PHP 8.1–8.4, pinned to the stable symfony/semaphore line that matches the framework's existing Symfony constraint (likely ^6.4 or ^7.x). Backend should be Redis reusing the app's existing connection; flock is only acceptable for single-host, non-containerised deployments and should be ruled out for anything running on more than one pod. The store abstraction means the backend is a config decision, not a code decision — keep it swappable.
Migration path
Do not let this land as a direct dependency inside job classes. Wrap it:
ConcurrencyGuard contract (run(string $key, int $permits, Closure $critical): mixed) owned by the platform team, with a finally-based release and a hard timeout.symfony/semaphore; keep the existing Cache::lock() implementation behind the same interface so the backend is reversible.SETNX/Cache::lock usages be consolidated onto it. That consolidation is where the real cleanup value is; the component itself is the small part.Compatibility
Validate the constraint graph before committing: composer why-not symfony/semaphore ^X against the current lock file. Watch for symfony/lock and predis/predis version interplay (Predis 2.x support was itself a bugfix in the 5.4/6.4/7.0 lines). Redis cluster/sentinel DSN handling requires 6.4.19+/7.2.4+ — a silent failure mode on older patches. Do not adopt 8.1 without a PHP 8.4 upgrade already scheduled.
Sequencing
ConcurrencyGuard wrapper behind a feature flag with the existing implementation as default.Maintenance
Low code churn — the component's public API has been stable across 6.4/7.x/8.x, and releases are largely "no significant changes." The maintenance burden sits upstream: the repo is a read-only split of symfony/symfony, so issues and PRs route to the monorepo and the package's own tracker is not a support channel. Upgrades ride the Symfony minor cadence; budget for a recurring review of advisories rather than a rewrite. The "looking for a backer" line in the README is Symfony boilerplate, not a funding risk signal — but it is worth knowing that no commercial support contract exists.
Support
Community-only, via Symfony's core team. That is strong for a component of this size and weak for anything time-sensitive. The mitigation is internal: a written runbook covering key naming conventions, TTL selection, release discipline, and what to do with a suspected stale permit. Institutional knowledge concentration is the real risk — two engineers should be able to operate it, not one.
Scaling
Throughput ceiling is Redis, not PHP: expect 1–10 ms per acquire/release round trip, so ~2–20 ms of added latency per guarded section. At high request rates that is a material budget, and contention on a hot key serialises throughput regardless of horizontal scaling. Design keys narrowly (render:{tenant_id}, partner_api:{integration_id}) — broad keys turn a concurrency control into a global bottleneck. Align worker counts with permit counts: if Horizon runs 20 workers against 5 permits, 15 workers are parked and the queue backs up invisibly. Watch Redis connection pool exhaustion under Octane.
Failure modes
acquire() in a web request under contention exhausts FPM workers. Prefer short non-blocking acquire with a graceful "busy, retry" response over long blocks.finally is mandatory or permits leak until TTL.Ramp-up
The API is learnable in an afternoon; the semantics are not. Budget a half-day of enablement on the three things engineers get wrong: destructor-based release, TTL longer than the critical section, and key granularity. Add a code-review checklist entry ("every acquire has a finally release and a justified TTL"). The most common failure is not a bug in the package — it is a team adopting a counting semaphore for a problem that needed a binary lock and paying contention costs for no benefit.
How can I help you explore Laravel packages today?