nilportugues/assert
A lightweight PHP assertion library for validating values and enforcing invariants. It provides expressive, chainable assertions that fail with clear exceptions, helping you catch invalid input early and make code contracts explicit and easy to read.
Install and inspect
composer require nilportugues/assert
The package has no Laravel service provider, facade, or config file — it is a plain PHP assertion helper. Before writing code, open the following in the repository (or vendor/nilportugues/assert after install):
src/ — the main Assert class (or similarly named class) is the entire public API surface.README.md — usually lists every assertion method and the exception each throws.tests/ — the fastest way to see real call signatures and edge-case behavior, especially around nulls, empty strings, and numeric strings.Confirm the minimum PHP version in composer.json. Release date is 2016-08-04, so expect a PHP 5.6/7.0-era codebase; verify it installs cleanly on your PHP 8.x before committing to it.
First use case
The natural day-to-day fit is guard clauses at the boundary of your domain code — value objects, DTOs, and service methods — not replacing Laravel's Validator for user-facing form input.
use NilPortugues\Assert\Assert; // verify exact namespace/class name in src/
final class Money
{
private int $amount;
public function __construct(int $amount)
{
Assert::integer($amount);
Assert::greaterThan($amount, 0);
$this->amount = $amount;
}
}
Start by wrapping one value object or one service method. Do not retrofit an entire application on day one; this package is unmaintained and you want a small, reversible blast radius if you abandon it later.
1. Assert at construction, not at use
The strongest pattern is to validate once in a constructor/factory and let the rest of the codebase trust the type. This pairs well with Laravel's container and readonly properties:
final class Email
{
public function __construct(public readonly string $value)
{
Assert::string($value);
Assert::email($value); // check method name in README
}
}
2. Bridge assertion exceptions into Laravel's error handling
The package throws its own exception class (commonly InvalidArgumentException or a package-specific AssertionException). Laravel will render that as a 500 unless you catch it. Convert at the boundary:
use Illuminate\Validation\ValidationException;
try {
$dto = new CreateOrderRequest($request->all());
} catch (\InvalidArgumentException $e) {
throw ValidationException::withMessages([
'order' => $e->getMessage(),
]);
}
A small AssertionsToValidation middleware or a base FormRequest::passedValidation() hook keeps this conversion in one place instead of sprinkled across controllers.
3. Use it inside FormRequest::after() for conditional rules
Laravel's rule strings get awkward for cross-field or domain-specific invariants. Use the assertion library as a post-validation guard:
public function after(): array
{
return [
function (Validator $validator) {
try {
new DateRange($this->input('start'), $this->input('end'));
} catch (\InvalidArgumentException $e) {
$validator->errors()->add('start', $e->getMessage());
}
},
];
}
4. Wrap it in a project-owned facade or helper
Because the upstream package is unmaintained, wrap it behind one class of your own (App\Support\Assert). That gives you a single file to change if you later swap to webmozart/assert, beberlei/assert, or hand-rolled checks — and lets you add Laravel-aware helpers (localized messages, ValidationException throwing) without forking.
5. Validate Artisan/queue payloads the same way
Jobs and console commands get the same guard-clause treatment. Fail fast in the job constructor so bad payloads never reach handle():
public function __construct(public array $payload)
{
Assert::array($payload);
Assert::keyExists($payload, 'user_id');
}
6. Pair with Laravel validation, don't replace it
Use Laravel's Validator for request shape, authorization, and user-facing messages; use the assertion package for invariants that must hold regardless of entry point (HTTP, queue, console, tests). This separation keeps validation messages translatable and keeps domain objects honest.
Unmaintained since 2016 — treat it as a vendored utility
The last release is from August 2016 and the repository is effectively abandoned. Expect:
create_function, dynamic properties).Run your test suite on the target PHP version before adopting. If anything breaks, wrap and replace rather than patch.
Exception types and HTTP status
The library's exceptions are generic PHP exceptions, not Illuminate\Validation\ValidationException or HttpException. Uncaught, they render as 500s. Always convert at the controller/request boundary. Check which exception class each assertion throws — some libraries throw InvalidArgumentException, others a custom AssertionFailedException; broad catch (\InvalidArgumentException) may or may not catch everything.
Method naming is inconsistent across assertion libraries
In this ecosystem you will find isString() vs string(), notEmpty() vs notBlank(), greaterThan() vs min(). Read the actual src/ file — do not assume based on webmozart/assert or beberlei/assert muscle memory. Method names are the number one source of "does nothing" bugs: a typo in a static call throws BadMethodCallException, a typo inside a fluent chain may silently pass.
Null and empty-string semantics
The most common production surprise. Verify in tests/ whether:
notEmpty('0') passes or fails.integer('42') (numeric string) passes.notNull and notEmpty are distinct.Domain objects should reject numeric strings deliberately — assert is_int() not is_numeric() when the type matters.
No localization, no field names
Messages are generic English strings and usually lack field context. Do not surface them directly to end users. Either map them in the ValidationException conversion (as above) or use them only for logs and 500-level "this is a programmer error" diagnostics.
Performance is fine; misuse is not
Static assertion calls are microseconds. The cost is architectural: assertions in hot loops or in boot() methods across hundreds of models will slow tests. Assert in constructors and at job/request boundaries only.
Testing expectations
Because assertions throw, your unit tests for value objects double as behavior documentation. Use expectException(\InvalidArgumentException::class) (or the actual class) and assert on the message. Add one test per assertion path — this is cheap and it pins the upstream behavior so a future replacement is safe.
Extension points are limited
There is no documented macro/extension mechanism in a package this small. If you need custom assertions (e.g., Assert::cpf() for Brazilian documents, which is plausible given the author), the intended approach is a subclass or a wrapper class in your own namespace that delegates to the library and adds methods. Do not edit vendor/.
Migration path if you outgrow it
The API is small enough to replace in an afternoon. webmozart/assert (now webmozart/assert maintained under Symfony) and beberlei/assert are the canonical maintained alternatives. Because you wrapped it in App\Support\Assert, swapping means rewriting one file and re-running the suite. Decide on this exit strategy before you sprinkle direct NilPortugues\Assert\Assert:: calls across 40 files.
How can I help you explore Laravel packages today?