A PHP library for managing distributed capacity control and queuing. It sits in front of capacity-limited resources, controlling how many users can access them simultaneously and queuing the rest.
# Tests (unit only — integration tests need Docker/Redis)
vendor/bin/phpunit tests/Unit/
# Static analysis (level 8)
vendor/bin/phpstan analyse --memory-limit=512M
# Coding standards (PSR-12 + Slevomat)
vendor/bin/phpcs
vendor/bin/phpcbf # auto-fix
# Rector (dry-run by default)
vendor/bin/rector process --dry-run
# Docker-based full test run (includes integration tests with Testcontainers)
docker run --rm -v $(pwd):/app -v /var/run/docker.sock:/var/run/docker.sock airlock php vendor/bin/phpunitComposer requires --ignore-platform-reqs locally (ext-redis, ext-memcached, ext-zookeeper are Docker-only).
Namespace: Clegginabox\Airlock — PSR-4 under src/.
Airlock— main contract:enter(identifier, priority): EntryResult,leave(identifier),getPosition(identifier),getTopic(identifier)ReleasingAirlock—release(SealToken)— for airlocks that support explicit slot releaseRefreshingAirlock—refresh(SealToken, ?ttl): ?SealToken— for extending leasesEntryResult— immutable value object returned byenter(). Eitheradmitted(has token) orqueued(has position)
| Class | Strategy | Implements |
|---|---|---|
OpportunisticAirlock |
First-come best-effort, no queue | Airlock, ReleasingAirlock |
RateLimitingAirlock |
Pure rate limiting, no release | Airlock |
QueueAirlock |
Fair queuing (FIFO or Lottery) with notifications | Airlock, ReleasingAirlock |
Seal::tryAcquire(): ?SealToken — the core locking primitive.
Sub-interfaces: ReleasableSeal, RefreshableSeal, LockingSeal (marker), RateLimitingSeal (marker), PortableToken (marker).
Implementations live in Bridge/Symfony/Seal/:
SymfonyLockSeal— wraps Symfony LockSymfonySemaphoreSeal— wraps Symfony Semaphore (N concurrent)SymfonyRateLimiterSeal— wraps Symfony RateLimiterCompositeSeal— chains a LockingSeal + RateLimitingSeal (both must pass)
QueueInterface: add, remove, peek, getPosition.
FifoQueue— strict FIFO, backed byInMemoryFifoQueueStoreorRedisFifoQueueStore(Lua scripts)LotteryQueue— random selection, backed byRedisLotteryQueueStoreBackpressureQueue— decorator that blocks admission whenHealthCheckerInterface::getScore()is below threshold
LoggingAirlock— wraps anyAirlock, logs via PSR-3LoggerInterfaceEventDispatchingAirlock— wraps anyAirlock, dispatches PSR-14 events
Both implement Airlock, ReleasingAirlock, RefreshingAirlock with runtime instanceof guards for release/refresh.
EntryAdmittedEvent— user gained accessEntryQueuedEvent— user was queuedUserLeftEvent— user voluntarily leftLockReleasedEvent— lock releasedLeaseRefreshedEvent— lease extended
AirlockNotifierInterface::notify(identifier, topic) — called by QueueAirlock::release() to alert the next person.
NullAirlockNotifier— no-op (polling-based or testing)MercureAirlockNotifier— real-time push via Symfony Mercure SSE
Bridge/Symfony/— Seal implementations (Lock, Semaphore, RateLimiter) + Mercure hub factoryBridge/Mercure/—MercureAirlockNotifierBridge/Laravel/— Service provider, facade, config
LeaseExpiredException— refreshing an expired tokenSealAcquiringException— seal acquisition failureSealReleasingException— seal release failure
- PHP 8.4,
declare(strict_types=1)on every file - PSR-12 + Slevomat rules (see
phpcs.xml): trailing commas, early returns, no Yoda, alphabetically sorted uses,::classover strings - PHPStan level 8
final readonly classfor value objects and leaf implementations- Interfaces for all extension points
- Composition over inheritance (CompositeSeal, BackpressureQueue, decorators)
- Tests: PHPUnit 13,
createMockForIntersectionOfInterfacesfor intersection types,#[AllowMockObjectsWithoutExpectations]attribute on tests with unused mocks
src/
Airlock.php, EntryResult.php, ReleasingAirlock.php, RefreshingAirlock.php
OpportunisticAirlock.php, QueueAirlock.php, RateLimitingAirlock.php
HealthCheckerInterface.php
Seal/ — Seal, SealToken, ReleasableSeal, RefreshableSeal, CompositeSeal, markers
Queue/ — QueueInterface, FifoQueue, LotteryQueue, BackpressureQueue, Storage/
Notifier/ — AirlockNotifierInterface, NullAirlockNotifier
Decorator/ — LoggingAirlock, EventDispatchingAirlock
Event/ — EntryAdmittedEvent, EntryQueuedEvent, UserLeftEvent, LockReleasedEvent, LeaseRefreshedEvent
Exception/ — LeaseExpiredException, SealAcquiringException, SealReleasingException
Bridge/
Symfony/Seal/ — SymfonyLockSeal, SymfonySemaphoreSeal, SymfonyRateLimiterSeal + tokens
Symfony/Mercure/ — SymfonyMercureHubFactory
Mercure/ — MercureAirlockNotifier
Laravel/ — ServiceProvider, Facade, config
tests/
Unit/ — mirrors src/ structure
Integration/ — Redis-backed tests (need Docker)
Factory/ — RedisFactory helper
examples/
app/ — Spiral Framework example app with multiple airlock strategies
enter(identifier) →
[OpportunisticAirlock] seal.tryAcquire() → admitted or queued(-1)
[RateLimitingAirlock] seal.tryAcquire() → admitted or queued(-1)
[QueueAirlock] queue.add() → position
if position == 1 → seal.tryAcquire() → admitted or queued(1)
if position > 1 → queued(position)
release(token) →
[QueueAirlock] seal.release() → queue.peek() → notifier.notify(next)