Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Normal Distribution Bundle Laravel Package

happyr/normal-distribution-bundle

A Symfony bundle for normal (Gaussian) distribution calculations. It provides reusable services for probability density, cumulative distribution, and related statistical values, making it easy to integrate Gaussian math into PHP applications via Symfony’s service container.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: despite the name, happyr/normal-distribution-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no auto-discovery. Roughly 90% of the repo is Symfony plumbing (DI extension, bundle class, config tree) wrapped around a small, framework-agnostic math class.

So the realistic adoption path in a Laravel app is:

  1. Install it, but ignore the bundle.
  2. Use Happyr\NormalDistributionBundle\Service\NormalDistribution directly (or vendor a copy — it's MIT).
composer require happyr/normal-distribution-bundle
use Happyr\NormalDistributionBundle\Service\NormalDistribution;

$nd = new NormalDistribution();

// API latency: mean 240ms, stddev 60ms. What's P(response > 800ms)?
$p = 1 - $nd->getCumulativeProbability(800, 240, 60);
// ~0.0000000... — you get the point

Where to look first:

Path Why
src/Service/NormalDistribution.php The only file that matters to you. ~100 lines of math.
src/DependencyInjection/Configuration.php Tells you what the happyr_normal_distribution defaults are (mean, standard_deviation) — you won't use them.
src/Resources/config/services.xml Service id is happyr.normal_distribution; irrelevant outside Symfony.

First real use case: turning a p95/p99 latency SLO into a concrete threshold, or the reverse. Given historical mean/stddev of a job's runtime, answer "what runtime do 95% of jobs finish under?" in one call rather than hand-rolling an approximation.

Verify method signatures against the source before wiring — the last tagged release is 2020-09-15 and the API is small but undocumented in the README.

Implementation Patterns

1. Bind it once, inject it everywhere

Don't new it at call sites. Register a singleton so you can swap it later.

// app/Providers/AppServiceProvider.php
use Happyr\NormalDistributionBundle\Service\NormalDistribution;

public function register(): void
{
    $this->app->singleton(NormalDistribution::class, fn () => new NormalDistribution());
}

If you want to be able to replace it with a maintained library, bind to your own interface:

interface Distribution
{
    public function cdf(float $x, float $mean, float $sd): float;   // P(X <= x)
    public function pdf(float $x, float $mean, float $sd): float;   // density at x
    public function inverse(float $p, float $mean, float $sd): float;
}

Then wrap the bundle's class in an implementation. This is the single highest-value pattern here — it costs 20 minutes and removes the "0 stars, last release 2020" problem from your critical path.

2. Percentile helpers (the everyday win)

The inverse-CDF call is what you'll actually repeat. Wrap it:

final class SloCalculator
{
    public function __construct(private Distribution $dist) {}

    /** Runtime that $pct of jobs finish under. */
    public function percentile(float $pct, float $mean, float $sd): float
    {
        // getValueFromProbability takes 0..1
        return $this->dist->inverse($pct / 100, $mean, $sd);
    }

    /** Probability a request exceeds $threshold. */
    public function exceedance(float $threshold, float $mean, float $sd): float
    {
        return 1 - $this->dist->cdf($threshold, $mean, $sd);
    }
}

Usage in a scheduled command:

$stats = DB::table('job_runs')
    ->where('created_at', '>', now()->subDays(7))
    ->selectRaw('AVG(duration_ms) as mean, STDDEV_SAMP(duration_ms) as sd')
    ->first();

$p99 = $calc->percentile(99, $stats->mean, $stats->sd);
$this->info("p99 job runtime: {$p99}ms");

Note STDDEV_SAMP returns null for a single row — a very common source of NaN/Inf downstream. Guard it (see Gotchas).

3. Anomaly scoring without a stats library

Z-score is the cheap pattern: "is this request unusual relative to this endpoint's own history?"

$z = ($actual - $mean) / $sd;

if ($z > 4) {
    Alert::dispatch("Endpoint {$route} at {$z}σ over baseline");
}

Per-route baselines beat a global threshold massively, and this is a two-line change. Store mean/sd in a endpoint_baselines table refreshed nightly.

4. Threshold tuning from an SLO

Reverse the usual direction — start from the budget and derive the alarm:

// 1% error budget → alarm at P99 of the good distribution
$threshold = $calc->percentile(99, $mean, $sd);

config(['monitoring.endpoint_threshold_ms' => $threshold]);

Do this per environment/endpoint, then have your alert rule read the config value. It removes the magic-number guessing that normally goes into config('monitoring.threshold').

5. Testing

Bind a deterministic fake in tests — never assert on floating-point tails.

$this->app->bind(Distribution::class, fn () => new class implements Distribution {
    public function cdf(float $x, float $m, float $s): float { return 0.5; }
    public function pdf(float $x, float $m, float $s): float { return 0.1; }
    public function inverse(float $p, float $m, float $s): float { return $m; }
});

Gotchas and Tips

It's a Symfony bundle — don't register it as a Laravel provider. Happyr\NormalDistributionBundle\HappyrNormalDistributionBundle extends Symfony\Component\HttpKernel\Bundle\Bundle. Adding it to config/app.php will fatal. Installing the package still pulls Symfony dependency-injection / http-kernel / config components into your vendor/ — harmless but noisy in composer outdated.

PHP version risk is real. Last release 2020-09-15 means a PHP 7.x-era constraint. On PHP 8.2/8.3 you may hit a platform requirement block at install, or deprecation notices (implicit nullable params, dynamic properties). Check composer show happyr/normal-distribution-bundle for the php constraint first; don't --ignore-platform-reqs your way past it in production without running the full test suite.

getProbability is the PDF, not a probability. It returns a density — it can exceed 1 for small σ, and integrating over it is what gives you probability mass. If you want "chance that X is below 800", you want getCumulativeProbability. Mixing these up is the #1 silent bug with this package. Rename them at your wrapper boundary.

Zero or near-zero standard deviation. σ appears in the denominator of the z-score. A constant metric (σ = 0, or null from an empty aggregate) yields Inf/NAN/division-by-zero. Always:

if (!is_finite($sd) || $sd < 1e-9) {
    return $actual > $mean ? 1.0 : 0.0; // degenerate distribution
}

This bites hardest in tests and on new endpoints with one sample.

The inverse CDF is an approximation. Implementations of this vintage typically use a rational approximation (Abramowitz & Stegun / Acklam-style). It's accurate to ~1e-9 in the body but degrades in the far tails (p > 0.9999) and can return values slightly outside expected ranges. Sanity-check against a reference (scipy, R, or markrogoyski/math-php) for your specific percentiles before you commit a p99.9 SLO to it.

Input domain: the CDF takes a raw value; the inverse takes 0 < p < 1. Passing 95 instead of 0.95 gives a nonsense result with no error. Validate at your wrapper.

Config defaults don't exist outside Symfony. The DI config lets Symfony users set mean and standard_deviation once; bypassing the container means every call needs the mean and σ passed explicitly. That's fine — usually correct, actually, since per-endpoint baselines are the whole point.

No caching, but you don't need any. These are microseconds of arithmetic. The expensive part is the SQL aggregate computing mean/σ, not the distribution call. Cache that (nightly materialization), not the math.

Extension points: effectively none. There's no strategy pattern, no pluggable backend. If you need anything beyond the five or six methods, you're writing it yourself. Which is why the wrapper-interface pattern above is the recommended shape — it makes swapping to markrogoyski/math-php (maintained, Math\Statistics\Distribution\Continuous\Normal) a one-file change.

Honest maintenance read: 0 stars, 0 dependents, no release in ~5 years. Treat it as a reference implementation to read and vendor (MIT permits it), not as a dependency you'll grow to rely on. The entire useful surface is one small class; copy it into app/Support/ with attribution, add the guards above, and you own it. That's a better trade than a dormant transitive dependency in a Laravel app — and it sidesteps the Symfony component bloat entirely.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings