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

Foundation Laravel Package

php-standard-library/foundation

Foundation provides the core building blocks for PHP Standard Library packages: a consistent exception hierarchy, the Ref reference wrapper for safe value passing, and invariant functions for validating preconditions and enforcing assumptions. Lightweight, dependency-free, and designed for reuse acr...

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require php-standard-library/foundation

That is the entire setup. There is no service provider, no config file, no publishable assets, no facade. The package is a set of namespaced classes (Psl\Ref, Psl\Exception\*) and, importantly, namespaced functions (Psl\Invariant\invariant(), precondition(), postcondition(), …) that Composer registers via a files autoload entry. If composer dump-autoload hasn't run, the functions simply won't exist — that's the only "installation" symptom you'll ever see.

Where to look first

  1. vendor/php-standard-library/foundation/src/Psl/Invariant/ — read the function signatures; they're short and self-documenting.
  2. vendor/php-standard-library/foundation/src/Psl/Exception/ — the marker interfaces (ExceptionInterface) and InvariantViolationException, which is the type you'll catch.
  3. vendor/php-standard-library/foundation/src/Psl/Ref.php — one class, one public $value property, generics annotated for PHPStan.

Ignore the GitHub repo's star count: this is a split package published from the php-standard-library/php-standard-library monorepo, so stars/dependents on the split are noise. Judge it by the monorepo's release cadence and the docs at php-standard-library.dev.

First use case: guard clauses in domain code

The highest-value, lowest-risk first adoption is replacing ad-hoc throw new \InvalidArgumentException(...) guards in actions, value objects, and DTO constructors:

use function Psl\Invariant\invariant;
use function Psl\Invariant\precondition;

final readonly class Money
{
    public function __construct(public int $amount, public string $currency)
    {
        invariant($amount >= 0, 'Money amount must be non-negative, got %d.', $amount);
        invariant(strlen($currency) === 3, 'Currency must be an ISO-4217 code.');
    }

    public function add(self $other): self
    {
        precondition($this->currency === $other->currency, 'Cannot add %s to %s.', $other->currency, $this->currency);

        return new self($this->amount + $other->amount, $this->currency);
    }
}

The distinction the package encodes — and the reason to prefer it over assert() or throw — is:

  • invariant() — this should be impossible. A bug in your code.
  • precondition() — the caller gave us something bad. A contract violation.
  • postcondition() — we produced something bad. A bug in your code.

All three throw Psl\Exception\InvariantViolationException.

Wire up a renderer immediately so these don't surface as raw 500s with stack traces in production:

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (InvariantViolationException $e, Request $request) {
        return response()->json([
            'title'  => 'Invariant violation',
            'detail' => app()->hasDebugModeEnabled() ? $e->getMessage() : 'An internal error occurred.',
        ], 500);
    });
})

Keep it in report() (do not add it to dontReport) — an invariant violation in production is a bug you want in Sentry.

Implementation Patterns

1. Guard clauses at the boundary of every value object

This is the bread-and-butter use. Constructors of value objects, enum-like wrappers, and command DTOs become self-validating. Combined with readonly properties (PHP 8.2+), you get objects that cannot exist in an invalid state — the thing Laravel's Validator fundamentally cannot give you, because Validator runs before the object exists and is opt-in at the call site.

use Psl\Ref;
use function Psl\Invariant\invariant;

final readonly class OrderId
{
    public function __construct(public string $value)
    {
        invariant(
            preg_match('/^ord_[0-9a-f]{26}$/', $value) === 1,
            'Malformed OrderId: %s',
            $value,
        );
    }
}

2. Ref as a mutable cell to avoid &$byReference and use (&$x)

Laravel code is full of $carry, use (&$errors), and closures that accumulate. Ref gives you an explicit, typed, greppable mutable cell:

use Psl\Ref;

$failures = new Ref([]);

foreach ($imports as $row) {
    try {
        $this->importRow($row);
    } catch (ValidationException $e) {
        $failures->value[] = $row['id'];
    }
}

if (count($failures->value) > 0) {
    // ...
}

It shines when you hand the cell to a callback the framework owns — Collection::each(), pipeline stages, event listeners, or a singleton factory that needs lazy memoization:

$this->app->singleton(RatesProvider::class, function ($app) {
    $loaded = new Ref();

    return new RatesProvider(
        loader: fn () => $loaded->value ??= $app->make(RatesClient::class)->fetch(),
    );
});

3. A thin Guard helper to translate to domain exceptions

InvariantViolationException is a single generic type. If you have a domain exception hierarchy (HTTP-mapped, translated, reported differently), wrap rather than spread:

namespace App\Support;

use Psl\Exception\InvariantViolationException;
use function Psl\Invariant\invariant;

final class Guard
{
    /**
     * @template T of \Throwable
     * @param class-string<T> $exception
     * @throws T
     */
    public static function that(bool $condition, string $exception, string $message, mixed ...$args): void
    {
        try {
            invariant($condition, $message, ...$args);
        } catch (InvariantViolationException $e) {
            throw new $exception($e->getMessage(), previous: $e);
        }
    }
}

Now Guard::that($user->isActive(), UserInactiveException::class, 'User %s is inactive.', $id) keeps the ergonomics and the framework-friendly exception type. This also means you can swap out the underlying package later with one file changed.

4. Exception handler integration for the marker interface

The package ships Psl\Exception\ExceptionInterface. Implement it on your expected domain exceptions so you can route them uniformly:

final class UserInactiveException extends \RuntimeException implements \Psl\Exception\ExceptionInterface {}

Then in bootstrap/app.php:

$exceptions->render(function (\Psl\Exception\ExceptionInterface $e, Request $request) {
    return response()->json([
        'type'   => 'https://errors.example.com/' . Str::kebab(class_basename($e)),
        'title'  => $e->getMessage(),
        'status' => 422,
    ], 422);
});

Anything not implementing the interface and not an InvariantViolationException falls through to Laravel's default handling — which is exactly what you want, because those are unexpected.

5. invariant() where you'd otherwise use assert()

Laravel/PHP projects use assert() for internal sanity checks, then someone sets zend.assertions=-1 in php.ini for production (the default for most distro builds!) and every assertion silently vanishes. invariant() is a real function call — it cannot be compiled away by an ini setting. Anywhere you currently have assert($x !== null) on a code path that "can't happen", use invariant().

6. Testing patterns (Pest / PHPUnit)

it('rejects a negative amount', function () {
    expect(fn () => new Money(-1, 'EUR'))
        ->toThrow(InvariantViolationException::class);
});

For tests that exercise the handler mapping, note that InvariantViolationException is not an HttpException, so $this->postJson(...)->assertStatus(500) is the correct baseline unless you've added the renderer above (then assert your 422/500 payload shape explicitly, so a future refactor doesn't silently change it).

7. Static analysis

PSL ships generics annotations (Ref<T>) and assertion annotations. With PHPStan at level 6+ you get Ref typing for free. If you want invariant() to narrow a type (?string → string), check whether the installed version annotates the function body with @phpstan-assert; if not, write a local stub in phpstan/stubs/ rather than sprinkling assert($x !== null) after every call.

Gotchas and Tips

You must import the functions. This is the #1 trip-up for Laravel devs used to Illuminate\Support\Str:

use function Psl\Invariant\invariant;   // required

Without it, invariant(...) resolves to a global function, and you get Call to undefined function invariant() — which looks like the package isn't installed when it is. Consider a project convention: always fully-qualify inline (\Psl\Invariant\invariant(...)) or always use function at the top; mixing both is where bugs happen.

Function autoloading and optimized autoloaders. composer install --optimize-autoloader is fine, but if you preload files (opcache.preload) or use a custom autoload bootstrap, remember the function file lives in files autoload, not PSR-4. A preload script that only registers class maps will break these functions at runtime.

Do not use invariant() for user input. Laravel's FormRequest/Validator gives you field-level messages, translated strings, and 422 responses. invariant() gives you a 500 and a dead request. The rule: user-supplied data → Validator; everything else → invariant/precondition. If you catch yourself writing invariant($request->input('qty') > 0, ...), you've picked the wrong tool.

Exception message leakage. invariant() accepts sprintf-style args, so messages routinely contain IDs, amounts, or internal field names. If your renderer returns $e->getMessage() unconditionally, you'll leak internals to API clients. Always gate on app()->hasDebugModeEnabled() or return a static string in production and let report() carry the detail.

Namespace collisions are not the problem — conceptual duplication is. Psl\ is a clean root; it will never collide with Illuminate\. The real risk is a codebase that validates the same invariant in three places: a FormRequest, a Money constructor, and a DB constraint. Pick the innermost layer (the value object) as the source of truth and let the outer layers be UX sugar.

Don't install the whole PSL stack "just in case." Foundation is deliberately tiny. Once you want Psl\Type\shape(), Psl\Vec\map(), or Psl\Result, you're pulling additional split packages that release in lockstep with the monorepo. Pin them together (same major version) to avoid a mixed 6.x/7.x dependency graph, and upgrade them as a set.

PHP floor. PSL tracks recent PHP closely — check the require.php constraint in composer.json before adding it to an older Laravel app. If your app is on PHP 8.1 you may need an older foundation major, and those older versions have different invariant signatures.

Ref is a mutable object — keep it out of serialized boundaries. It has no JsonSerializable implementation and does not survive serialize() meaningfully in queued jobs, Livewire payloads, or cache writes. Use it as an in-memory cell within a single request/process; once state must cross a boundary, convert to a plain value.

Two Ref instances never share state (obvious), but one Ref shared into a closure silently does. That's the feature — and the footgun. If you pass the same Ref into an event listener and a test assertion, ordering matters. Prefer creating the Ref at the narrowest possible scope, exactly where you'd have used &$x.

Debugging an unexplained invariant failure. Because these throw from deep inside constructors, the stack trace is often 4–5 frames away from the actual input. Add ->withExceptions(fn ($e) => $e->dontFlash([...])) if the failure happens during form submission and you don't want the payload flash-round-tripping, and log the constructor arguments in a local report() callback when the class is a value object. A custom context() on your wrapper exceptions is the cleanest fix if you adopt pattern #3.

Extension point: your own exception hierarchy. The package gives you ExceptionInterface and InvariantViolationException — nothing else. There is no "exception mapper" abstraction to hook into. That's a feature: the intended extension point is you implementing Psl\Exception\ExceptionInterface on your domain exceptions and writing one render() closure. Don't wait for a Laravel integration that will never ship.

Maturity assessment. The split package has effectively zero independent adoption signal, so treat abandonment risk as real but bounded: Ref and the invariant functions are ~200 lines of trivial, dependency-free code. If the monorepo ever stalls, vendoring Ref.php and a 20-line invariant() is an afternoon. The genuine value is the convention (the invariant/precondition/postcondition trichotomy) more than the code — which is why pattern #3's Guard wrapper is worth writing even if you later drop the dependency.

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.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
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