rskuipers/php-assumptions
PHP Assumptions is a static analysis tool that detects hidden assumptions in your code, like variables used without guarantees about their type or existence. It reports each assumption, helping you catch risky spots and improve type safety. Runs via Composer and CLI.
rskuipers/php-assumptions is a small, framework-agnostic library for expressing and enforcing assumptions — the invariants your code silently relies on ("this array has a id key", "this service returns a non-empty collection", "this JSON payload has an email"). Instead of a bare assert() or an ad-hoc if (...) throw, you declare the assumption in a readable, chainable way and get a consistent failure mode when reality disagrees.
Minimal steps
composer require rskuipers/php-assumptions (PHP 8.x; MIT; released March 2025, so it is current).app/Exceptions/Handler.php (or bootstrap/app.php in Laravel 11+) if you intend to catch it — see Gotchas.First use case
The highest-value first use is a boundary guard in a service class that receives data the framework has not fully validated: a webhook payload, a third-party API response, or a queued job's array payload. Something like:
use Rskuipers\Assumptions\Assumptions; // confirm exact namespace in the README
final class SyncInvoice
{
public function __invoke(array $payload): Invoice
{
Assumptions::check($payload)
->hasKey('id')
->hasKey('customer.email')
->that($payload['id'])->isString();
// ... from here on, trust the shape
}
}
Pick exactly one such call site, run the happy path and a deliberately malformed payload, and confirm the failure is loud and points at the right line. That five-minute exercise tells you more than the docs.
1. Guard the edges, trust the core.
The idiomatic split in a Laravel app is: Form Requests / Validator for user input (they produce 422s and field-level messages), assumptions for everything else — HTTP client responses, queue payloads, config arrays, filesystem reads, cache values. Assumptions are for "this should be impossible", not "the user might have typed this".
2. Action and service classes. The natural home is the top of an invokable action or a domain service — one assumption block per public method, immediately after the signature, before any real work. This gives readers a contract and gives you a stack trace that points at the exact method that received the bad value rather than 40 lines downstream.
3. DTO / value-object hydration.
When you map an array (from Http::get()->json(), a job payload, or an event) into a DTO, assert the shape once during hydration and then let the DTO carry the guarantee. This is the pattern that pays for the dependency fastest — it lets the rest of the codebase drop defensive ?? and isset() noise.
4. Jobs and console commands.
Queued jobs deserialise from an array you do not control once it has been sitting on the queue. A single assumption at the top of handle() turns a confusing Undefined array key deep in a model into a named failure with the offending payload in the trace.
5. Testing against them. Because assumptions throw a dedicated exception, tests become explicit: assert the happy path returns, and assert the exception is thrown for each malformed shape. Add a small data provider of broken payloads — it doubles as documentation of what the boundary rejects.
6. Keep a helper if you use it a lot.
If your call sites get verbose, wrap the facade in a project-level helper (app/Support/assume.php or a static Guard class) so the import lives in one place. This also lets you swap or remove the library later without touching dozens of files.
7. Complements static analysis, doesn't replace it.
Assumptions are runtime checks. PHPStan/Larastan will not understand that a check narrows a mixed type, so you still need @param docblocks or an inline /** @var array{id: string} $payload */ after the assumption to keep analysis clean.
Validator instead.assert() is not the same thing. zend.assertions=-1 in production silently disables the built-in assertions, which is exactly the trap this package exists to avoid. Confirm the library always evaluates (check whether it has any environment toggling) — that is the main behavioural question worth verifying in the source.renderable/respond branch rather than a global catch.files autoload, be aware they load on every request and can collide with your own helpers. Prefer the class-based API if both exist.mixed access errors and you will be tempted to sprinkle ignores.Collection"), write tiny named constructors or your own helper around the package rather than forking — the library is deliberately minimal, and its 3.7 score reflects "does one thing, small surface" rather than "batteries included".^1.0 blindly.How can I help you explore Laravel packages today?