Strict Validation
Every WireKit component with a prop enum (<x-wirekit::button intent="primary">, <x-wirekit::alert variant="info">, <x-wirekit::hero size="lg">, …) routes invalid values through a single decision point: the StrictnessGate. The gate decides whether to throw, log, and which value to render when a developer typo lands in production code.
The decision depends on the runtime context. That is intentional — a typo in a Pest test should fail fast and visibly; the same typo on a live HTTP request should not 500 the whole page over a cosmetic glitch. This page documents the full matrix so you can decide which mode fits each environment.
The decision matrix
Two configuration knobs interact with the runtime context:
| Knob | Default | What it does |
|---|---|---|
wirekit.validation.strict |
null (auto-detect from APP_DEBUG) |
When null, the gate is strict if APP_DEBUG=true, lenient otherwise. Set explicitly to true or false to override the auto-detect. |
wirekit.validation.throw_on_invalid |
false |
When true, the gate throws even in HTTP requests under strict mode. Useful for staging environments that should fail loudly. |
The resulting behavior:
| Runtime context | strict=true (or APP_DEBUG=true) |
strict=false (or APP_DEBUG=false) |
|---|---|---|
| HTTP request | log ERROR + render fallback |
log WARNING + render fallback |
CLI / Pest / artisan tinker |
throw InvalidArgumentException |
log WARNING + render fallback |
throw_on_invalid=true (any context) |
throw InvalidArgumentException |
throw InvalidArgumentException |
Fallback semantics: when the gate renders a fallback value, it picks the first allowed enum entry. For <x-wirekit::button surface="weird-typo"> the fallback is surface="filled". Each component can override the fallback per prop; the override surfaces in the same log line.
Why the split exists
A naive "throw on any typo" policy looks safe in isolation but creates a worse production failure mode: one cosmetic prop typo (a developer wrote surface="outlined" instead of surface="outline") takes down the entire Blade view with a 500 error. The page that should have rendered with a slightly-wrong button now renders nothing at all.
The matrix above resolves the trade-off:
- In a Pest test, the throw surfaces the typo at the assertion that should have caught it. No silent log line; no rendered fallback the test then accidentally validates.
- In
php artisan tinker, the throw matches what a developer would expect from a debugger — you typed the wrong thing, you get a stack trace. - In a live HTTP dev request (
APP_DEBUG=true), the page renders with the fallback so you can see the rest of the layout, and thelocal.ERROR: WireKit [...]line tells you exactly what went wrong. - In production, the log line drops to
WARNING(less noisy) but the fallback still keeps the page alive.
Spotting silent typos in logs
Two options:
Option 1 — let wirekit:doctor scan the log for you. The doctor's environment-tier checks include an optional silent-typo log scan that walks storage/logs/laravel*.log for the WireKit [...] ERROR / WARNING lines emitted by StrictnessGate. A WARN with example lines surfaces every typo that shipped:
# 1. The environment-tier checks include the log scan.
php artisan wirekit:doctor --tier=environment
The scan SAFE-DEGRADES at every failure mode — missing log file, custom log channel (Slack / Papertrail / Sentry), log-level filtering below WARNING — it surfaces an INFO line explaining the skip and never blocks the doctor's exit code. Disable entirely by setting wirekit.doctor.scan_logs to false (or WIREKIT_DOCTOR_SCAN_LOGS=false in .env) when your app doesn't write file-based logs.
Option 2 — grep the log directly. Lenient mode logs at WARNING level; strict-HTTP logs at ERROR. Either way the lines look the same:
# 1. From your project root, scan today's log for any WireKit fallback events.
grep 'WireKit \[' storage/logs/laravel.log
# 2. Narrow to one component if you suspect a specific surface.
grep 'WireKit \[button\]' storage/logs/laravel.log
# 3. Tail the log live while you click through the app to catch typos as they fire.
tail -f storage/logs/laravel.log | grep --line-buffered 'WireKit \['
A typical line looks like:
[2026-05-28 06:54:53] local.ERROR: WireKit [button]: Invalid surface "outlined". Allowed: filled, outline, soft, ghost, link. Falling back to "filled".
The Falling back to "..." suffix is the source of truth for what the page actually rendered.
Opting into strict mode for staging
Staging environments typically run with APP_DEBUG=false to mirror production, which puts the gate into lenient mode. If you would rather fail loudly on a typo before it reaches end users, flip the two knobs explicitly in your staging .env:
# 1. Force the gate into strict mode regardless of APP_DEBUG.
WIREKIT_STRICT_VALIDATION=true
# 2. Force a throw on every invalid value — no fallback, full stack trace.
WIREKIT_THROW_ON_INVALID=true
Add the corresponding config wiring to config/wirekit.php:
<?php
// 1. The 'validation' block lives at the top level of the wirekit config.
return [
// …
'validation' => [
// 2. null = auto-detect from APP_DEBUG. true/false = explicit override.
'strict' => env('WIREKIT_STRICT_VALIDATION'),
// 3. When true, the gate throws even in HTTP — page 500s on invalid props.
'throw_on_invalid' => env('WIREKIT_THROW_ON_INVALID', false),
],
// …
];
Be careful with throw_on_invalid=true in production — every invalid prop value will 500 the request that triggered it. The setting is useful in CI's smoke-test job and dangerous in front of real visitors. You must leave it false in production unless you have a deliberate fail-loudly policy.
Testing strict mode in Pest
CLI context already triggers the throw branch when strict mode is active. A typical regression test reads:
<?php
// 1. Pest browser test asserting an invalid prop value throws under strict mode.
use Illuminate\Support\Facades\Blade;
it('rejects unknown surface values on <x-wirekit::button>', function () {
// 2. Force strict mode explicitly so the test is independent of APP_DEBUG.
config()->set('wirekit.validation.strict', true);
// 3. The render call throws because 'outlined' is not in the surface enum.
expect(fn () => Blade::render('<x-wirekit::button surface="outlined">x</x-wirekit::button>'))
->toThrow(InvalidArgumentException::class, 'Invalid surface "outlined"');
});
The exception message carries a Levenshtein-ranked Did-you-mean hint — Did you mean "outline"? — so the failing assertion already points at the likely correct value.
How intent and surface interact under the gate
Components like <x-wirekit::button> and <x-wirekit::badge> carry two orthogonal enums (intent + surface). The gate runs each prop independently — an invalid intent falls back to primary, an invalid surface falls back to filled, and a value that lands outside both enums logs two separate lines. See Variants & Intents for the full prop-value catalog.
Asking the gate instead of reading its log
The gate's warnings are written for a developer reading a log. Two of the questions behind them are also available as plain predicates you can assert on in your own test suite — useful when you want a build to fail rather than a line to appear.
Both return an answer and nothing else: no logging, and no app.debug gate. That matters,
because the warnings themselves return early outside debug mode, so in production the
signal does not exist at all and no test can reach it.
use Pushery\WireKit\Support\StrictnessGate;
// Attribute names that are neither declared props nor legitimate HTML passthrough.
StrictnessGate::unknownPropNames($attributes, $declaredProps); // => ['varaint']
// Scope directives this component will DISCARD, because it sets its own on the
// same element and HTML keeps the first of two identical attributes.
StrictnessGate::discardedScopeDirectives('dropdown', ['x-data' => '{ open: false }']);
// => ['x-data']
discardedScopeDirectives() reads the component's own Blade template and asks whether it
sets the directive unconditionally — a component that only sets x-data inside a
condition does not always discard yours, and answering otherwise would report a collision
that does not happen. That distinction is why asking WireKit is better than re-deriving the
answer from the templates yourself.
A discarded x-data is not a degradation, it is a disconnection. Your scope never comes
into existence, so everything written against it — an x-init beside it, a @click inside
it — silently resolves against the component's own data. Wrap the component in your own
element, or use the API the component already exposes.
See Also
- Variants & Intents — canonical prop-value reference
- Customization — overriding component defaults via config
- CLI Reference —
wirekit:doctorand the wider Artisan surface