davedevelopment/stiphle
Stiphle is a small PHP library that turns any string into a clean, URL-friendly unique identifier. It slugifies text and automatically resolves collisions by appending numbers, making it handy for generating unique slugs, IDs, or filenames from titles and names.
composer require davedevelopment/stiphle
Minimal, single-process usage:
use Stiphle\Throttle\LeakyBucket;
$throttle = new LeakyBucket;
// Allow 5 calls per 1000ms, keyed by API token
$wait = $throttle->getThrottle('api:'.$token, 5, 1000);
if ($wait > 0) {
usleep($wait * 1000); // returned value is milliseconds
}
That is the whole library surface you need on day one: getThrottle($key, $limit, $windowMs) returns 0 if you are under the limit, or the number of milliseconds you should wait before retrying.
Where to look first:
src/Stiphle/Throttle/LeakyBucket.php — the only throttle implementation worth reading; understand leak() and calculateWait().src/Stiphle/Storage/ArrayStorage.php — default storage; useful for tests, useless across requests.src/Stiphle/Storage/ — Redis/Memcached/Mongo adapters; check which one matches your Laravel cache driver.src/Stiphle/Lock/ — the LockInterface used to protect the read-modify-write cycle.First use case: throttling calls to a third-party API (Stripe, GitHub, an internal service) from a wrapper client, before you reach for a full queue-based backoff.
Bind a configured throttle in a service provider. The default ArrayStorage resets every request, so in Laravel you must swap it for shared storage:
// app/Providers/AppServiceProvider.php
$this->app->singleton(LeakyBucket::class, function ($app) {
$throttle = new LeakyBucket;
$throttle->setStorage(new \Stiphle\Storage\Redis($app['redis']->connection()));
$throttle->setLock($app->make(App\Throttle\RedisLock::class));
return $throttle;
});
HTTP middleware for a single route group. Resolve the throttle, derive a key from user/IP/token, and return 429 with Retry-After when $wait > 0:
public function handle($request, Closure $next)
{
$key = 'throttle:'.($request->user()?->id ?? $request->ip());
$wait = $this->throttle->getThrottle($key, 60, 60000);
if ($wait > 0) {
return response('Too Many Requests', 429)
->header('Retry-After', ceil($wait / 1000));
}
return $next($request);
}
Outbound client decorator. Wrap each upstream call and sleep inline; this is where a leaky bucket shines versus RateLimiter::attempt, because it tells you how long to wait rather than just failing:
public function request(string $endpoint, array $payload)
{
$key = "upstream:{$this->vendor}:{$this->accountId}";
$wait = $this->throttle->getThrottle($key, 100, 60000);
if ($wait > 0) {
usleep($wait * 1000);
}
return $this->http->post($endpoint, $payload);
}
Queue worker throttling. Same call in a job's handle(). Because the leaky bucket is smooth rather than windowed, workers naturally spread out instead of bursting at second boundaries — usually what you want when hammering an upstream.
Namespace your keys relentlessly. "throttle:{$scope}:{$id}". Mixing IPs, user IDs and API keys in one namespace will silently cross-credit consumption.
Testing. Inject ArrayStorage and freeze time by manipulating the storage value directly, or just assert that getThrottle returns 0 up to the limit. Avoid real sleeps in the test suite.
getThrottle consumes budget even when it returns a wait. A retry loop that calls getThrottle, ignores the wait, and calls again will dig the bucket deeper. Always usleep/sleep the returned milliseconds, or abandon the request.ArrayStorage is per-request only. Every web request starts from zero. If your rate limiting "doesn't work" in production but works in a single CLI script, this is why.LeakyBucket reads, mutates and writes the bucket; under concurrency you undercount. Supply a LockInterface implementation backed by the same store (Redis SET NX PX, Memcached add, database advisory lock). Storage adapters do not lock for you.key, limit, windowMs. It reads as "5 calls per 1000ms". Mixing up count and window is the most common first bug.microtime() internally; there is no injectable clock. Tests and clock-skewed containers can behave oddly. Keep app hosts NTP-synced; don't test throttle timing by racing real seconds.Illuminate\Cache\RateLimiter. If you need classic fixed windows and framework-native middleware, use that instead; reach for Stiphle when you specifically want smooth leaky-bucket pacing or a "how long to sleep" answer.Stiphle\Storage\StorageInterface (get/set/lock/unlock) to back the bucket with your own store, or Stiphle\Lock\LockInterface for atomicity. Wrapping LeakyBucket in a decorator for logging/metrics is safe — it is not final.getThrottle call. Watching the counter leak downward over time is the fastest way to build a correct mental model — and to spot a storage adapter that is silently truncating floats.How can I help you explore Laravel packages today?