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

Assert Laravel Package

beberlei/assert

A simple PHP library of assertions and guard methods for fast, low-level input validation in business models, libraries, and application code. Failed assertions throw exceptions, eliminating if-clauses. Install via Composer: composer require beberlei/assert.

View on GitHub
Deep Wiki
Context7

Getting Started

beberlei/assert is a framework-agnostic, zero-dependency guard-clause library. There is nothing to register, no config to publish, no facade — it is just static methods that throw when a precondition fails.

composer require beberlei/assert

The first place to look is the assertion list in the README, and specifically the two APIs:

  • Static: Assert\Assertion::integer($value, 'message') — one check per call.
  • Fluent: Assert\Assert::that($value)->notEmpty()->integer() — many checks, value specified once.

Your first real use case should be a single Action/Service class where you currently have defensive if blocks at the top:

<?php

namespace App\Actions;

use Assert\Assert;

class RefundOrder
{
    public function __invoke(int $orderId, int $amountCents, string $reason): void
    {
        Assert::that($orderId)->integer()->greaterThan(0);
        Assert::that($amountCents)->integer()->greaterThan(0)->lessOrEqualThan(100_000);
        Assert::that($reason)->string()->notBlank()->maxLength(255);

        // ... from here on, arguments are guaranteed sane
    }
}

Because a failure throws Assert\AssertionFailedException, the happy path stays flat and readable. Do not reach for this to replace Laravel's Validator for HTTP input — that is a different job (see Gotchas).

Implementation Patterns

Guard clauses at the boundary of trusted code

The idiomatic use is "assert once at the entry point, then never check again". Put assertions at the top of Actions, Jobs, Console Commands, and Service methods. Everything after them runs in a known-good world.

public function handle(SyncPayload $payload): void
{
    Assert::that($payload->endpoint)->url();
    Assert::that($payload->retries)->integerish()->between(0, 10);
    Assert::that($payload->tags)->isArray()->all()->string();
}

Value objects with constructor invariants

This is where the library pays for itself. Assert in the constructor, and the object can never exist in an invalid state — no scattered re-validation downstream.

final class Money
{
    public readonly int $cents;

    public function __construct(int $cents, public readonly string $currency)
    {
        Assert::that($cents)->integer()->greaterOrEqualThan(0);
        Assert::that($currency)->string()->length(3)->regex('/^[A-Z]{3}$/');
        $this->cents = $cents;
    }
}

Laravel's casting layer (casts(), custom CastsAttributes) is a natural pairing: hydrate from raw attributes, let the constructor reject garbage, and the failure surfaces as an exception rather than an invalid model.

Chaining, nullOr and all

Three helpers cover most of the awkward cases:

Assert::that($value)->nullOr()->string()->startsWith('sku_');  // nullable field
Assert::that($ids)->all()->integerish();                        // every element
Assert::that($value)->notEmpty()->integer();                    // plain chain

Use Assert::thatNullOr() and Assert::thatAll() as shorthand constructors.

Lazy assertions for multi-field input

When you are validating a DTO or a form-shaped payload and want all the errors rather than the first one, use the lazy API and bridge it into Laravel's ValidationException:

use Assert\Assert;
use Assert\LazyAssertionException;
use Illuminate\Validation\ValidationException;

$lazy = Assert::lazy()->tryAll()
    ->that($data['email'] ?? null, 'email')->notEmpty()->email()
    ->that($data['age'] ?? null, 'age')->nullOr()->integerish()->between(18, 120)
    ->that($data['name'] ?? null, 'name')->notEmpty()->maxLength(80);

try {
    $lazy->verifyNow();
} catch (LazyAssertionException $e) {
    throw ValidationException::withMessages(
        collect($e->getErrorExceptions())
            ->mapWithKeys(fn ($ex) => [$ex->getPropertyPath() => $ex->getMessage()])
            ->all()
    );
}

The property path passed as the second argument to that() becomes the field name in the error bag, so choose it to match your form/DTO keys. getErrorExceptions() returns the raw AssertionFailedException instances (value + constraints included) if you need structured error details.

A project-local assertion subclass

The README explicitly recommends subclassing so a BC break or behaviour change in the library can't silently alter your domain semantics. In Laravel, do this in App\Support\Assert and register the exception class once at boot:

namespace App\Support;

use Assert\Assertion as BaseAssertion;

class Assertion extends BaseAssertion
{
    // override stringify() for nicer "integer given" text,
    // override createException() for a domain-specific exception
}
// AppServiceProvider::register()
\Assert\Assertion::setExceptionClass(\App\Exceptions\DomainAssertionException::class);

Note that setExceptionClass() is process-global static state — set it in register(), before anything else boots in the container.

satisfy() for domain rules with no built-in assertion

Assert::that($card)->satisfy(
    fn (string $v) => preg_match('/^\d{13,19}$/', $v) && $this->luhn($v),
    'Card number is not a valid PAN.'
);

Where not to use it

  • Controllers validating $request->all() — use FormRequests and Laravel's Validator, which gives you translations, :attribute substitution, and 422 responses for free.
  • Blade / user-facing messaging — assertion messages are technical English.
  • "Exists in database" / unique checks — those are query-shaped rules, not low-level assertions.

Gotchas and Tips

The value is always the first parameter, the message always the last. Assertion::max($value, 42, 'too big'), not max(42, $value). Easy to get backwards when an assertion takes two operands.

Failures render as HTTP 500 in Laravel, not 422. Assert\InvalidArgumentException extends SPL InvalidArgumentException, so Laravel's exception handler treats it as an unhandled server error. Either catch it at the boundary and convert to ValidationException (as above), or give your custom exception class a render() method. Catching the interface Assert\AssertionFailedException is the safest catch clause since the concrete class is configurable.

Functional constructors bypass your subclass. Assert\that(), Assert\thatAll(), Assert\thatNullOr() and Assert\lazy() are hard-wired to the base Assertion class. If you need a custom assertion/exception, use the static Assert::that() / Assert::lazy() API, or re-implement the four tiny functions yourself.

nullOr* and all* are __callStatic magic. PhpStorm will not autocomplete Assertion::nullOrMax() or Assertion::allIsInstanceOf(), and a typo surfaces as a runtime error rather than a compile-time one. Prefer the fluent ->nullOr() / ->all() chain in new code, which at least fails on a method call you can grep.

integer is strict, integerish is not. Assertion::integer('42') throws. Assertion::integerish('42') passes. HTTP input, query strings, and env vars are almost always integerish; DB-derived values are integer.

true/false are identity checks. Assertion::true(1) and Assertion::true('1') both fail. Use Assertion::boolean() to assert the type, and eq()/same() for value comparison.

Lazy assertions need tryAll() per value to collect multiple failures on one field. Without it, a chain stops at the first failure for that property path. Calling tryAll() once on the lazy builder applies it globally and is the cleaner form.

verifyNow() throws; it does not return a result. If you want to inspect errors before deciding, call verifyNow() inside a try/catch or work with the lazy object directly. Don't call it twice expecting a reset.

Message callbacks get an array, not a string. You can pass a closure as $message and it will only be invoked on failure. The array contains ::assertion plus the operands — inspect that key when composing the message, because assertions internally delegate (e.g. nullOrMax calls max).

length/minLength/maxLength depend on mbstring. Without mbstring these degrade to byte length and will reject multi-byte input at the wrong threshold. Laravel ships with mbstring in practice, but verify in your CI matrix, and remember ctype-based checks (alnum, digit) are locale-sensitive for non-ASCII.

email and url delegate to filter_var. They are permissive and will accept addresses/URLs stricter validators reject. Fine as a cheap sanity check on internal data; do not treat them as the authority for public signup forms.

Filesystem assertions (file, directory, readable, writeable, noContent) touch disk. In a hot loop these become a real cost; hoist them out of iterations.

Assertions are always on. There is no assert.active switch — this is not PHP's assert(). That is the point (they must run in production), but it means every guard costs something on every request. They are cheap; just don't put a hundred of them in a per-item loop without thinking.

Pin the version. New assertions and tightened checks land in minor releases (see the changelog), and a "more correct" check can start rejecting data your code previously accepted. Treat upgrades like any other behavioural dependency and run your test suite against the new tag.

Test them like any other thrown exception. $this->expectException(\Assert\InvalidArgumentException::class) before the call, or in Pest expect(fn () => new Money(-1, 'EUR'))->toThrow(...). Asserting on getValue() and getConstraints() is a good way to document the failure contract.

Extension points are limited but sufficient: subclass Assertion, override stringify() for message formatting, override exception creation, and use satisfy() for anything bespoke. There is no rule registry — don't try to build one on top of it; you'll end up re-implementing Laravel's Validator badly.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment