wikimedia/alea
Alea is a PHP port of the Alea pseudo-random number generator, providing fast, seedable, deterministic random values. Useful for simulations, games, and tests where reproducible output matters.
wikimedia/alea is a small, dependency-free PHP library from the Wikimedia Foundation for deterministic sampling — given an identifier and a rate, it decides whether that identifier is "in" or "out" of a sample, consistently. It ships no Laravel service provider, no config file, and no facade. That is the whole point: it is a primitive, and your job is to wrap it once and then use it everywhere.
Install:
composer require wikimedia/alea
Where to look first:
vendor/wikimedia/alea/src/ — the whole library is a handful of classes; read them before writing any wrapper. The public surface is tiny and you should know it exactly.vendor/wikimedia/alea/README.md — for the constructor signature and rate semantics on your pinned version.gerrit.wikimedia.org/g/mediawiki/libs/alea), not GitHub. The GitHub repo is an automated mirror; issues and patches live in Phabricator/Gerrit.First use case — deterministic feature rollout. The canonical day-one win is replacing ad-hoc mt_rand(1, 100) <= $percent checks, which are non-deterministic per request and therefore produce a flickering experience for a single user:
use Wikimedia\Alea\Sampler;
$sampler = new Sampler(0.05); // 5% sample rate
if ($sampler->sample($request->user()->id)) {
// this user is consistently in the 5% bucket, on every request
}
Because the decision is a pure function of (rate, key), the same user always lands in the same bucket — which is exactly what you want for feature flags, experiments, and log-volume reduction.
Never new Sampler(...) inside a controller. Put the rate in config and resolve it through the container:
// config/alea.php
return [
'features' => [
'new_checkout' => (float) env('ALEA_NEW_CHECKOUT_RATE', 0.0),
'async_webhooks' => (float) env('ALEA_ASYNC_WEBHOOKS_RATE', 1.0),
],
];
// app/Providers/AleaServiceProvider.php
public function register(): void
{
$this->app->singleton(SamplerFactory::class, fn () => new SamplerFactory(config('alea.features')));
}
Then a thin SamplerFactory returns a Sampler per feature name. This gives you one place to change rates, one place to audit them, and one place to stub them in tests.
The quality of the whole system depends on the key you feed it. Good keys: user ID, a UUID stored in a cookie, an account creation hash. Bad keys: now(), request()->ip() (changes on mobile networks), session()->getId() (regenerated on login), anything from random_bytes().
For anonymous traffic, mint a UUID in a long-lived cookie on first visit and use that as the key — otherwise logged-out users flip buckets on every session.
Decide once per request, stash the decisions on the request, and let the rest of the stack read them:
public function handle($request, Closure $next)
{
$key = $request->user()?->id ?? $request->cookie('visitor_id');
$request->attributes->set('alea', $this->factory->decideAll((string) $key));
return $next($request);
}
Downstream: $request->attributes->get('alea')['new_checkout']. This keeps the sampler out of your views and controllers and makes the decisions trivially loggable in one place (which you will want when someone asks "why did this user see the old checkout?").
A sampler answers a boolean. For A/B/C, run independent samplers with cumulative rates, or derive a single bucketing value once and slice it yourself:
0.00 – 0.49 → control
0.50 – 0.79 → variant A
0.80 – 1.00 → variant B
Wrap this in an Experiment class that maps a key to an arm name. Record the arm in your analytics event alongside the user ID so the analysis is reproducible.
This is where the library quietly pays for itself. Before shipping a high-volume log or metric to an external sink, sample it:
if ($this->sampler->sample($event->id)) {
Metrics::push($event);
}
At 1% you cut ingestion cost by two orders of magnitude and still get statistically valid rates — provided you sampled on a stable key and not on rand(). If you sample per event rather than per user, be explicit about it; per-event sampling is fine for counting, per-user sampling is mandatory for anything behavioural.
Background work frequently needs to sample at much higher rates than web traffic (or the inverse — you may want to exclude sampled users from a backfill). Pass the same key through the job payload so the decision is stable between the web request and the queued job. In commands, use a stable key like the tenant ID or a batch ID, not the process PID.
Never let real sampling leak into tests. Bind a fake in TestCase::setUp():
$this->app->bind(SamplerFactory::class, fn () => new AlwaysSampleFactory());
$this->app->bind(SamplerFactory::class, fn () => new NeverSampleFactory());
Then write a small number of property tests against the real library — same key always yields the same answer; rate 0.0 never samples; rate 1.0 always samples; a large population at rate 0.1 lands near 10% — and keep everything else faked.
int weights and float rates across Wikimedia libraries. Read the source in vendor/ once and write a single factory wrapper so a future signature change touches one file.0.0 and 1.0 are edge cases that have historically been off-by-one bugs in weighted samplers. Assert both explicitly in tests, and prefer 0.0 over 0.00001 for "off" and 1.0 over 0.999 for "on".config:cache and provider ordering. Read config('alea.*') inside the closure passed to singleton(), not as a constructor argument evaluated at boot. Otherwise a cached config baked at deploy time silently pins yesterday's rates.0.0, not to a lower rate, unless you actually want a churn of users flipping in and out.ALEA_FORCE=1 / ALEA_FORCE=0 override into your factory early; you will use it daily.Sampler is trivially injectable and trivially fakeable. A facade adds indirection and makes the stable-key discipline harder to enforce.How can I help you explore Laravel packages today?