Skip to main content
WireKit
Copy for LLM

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 the local.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

Was this page helpful?

Voting requires cookies or local storage. What we store