Preset exemptions
Five presets accept an exemption list — strictTypes, finalByDefault, noLocalCryptoPrimitives, noDebuggingLeftovers and modelsGoThroughTheFacade. Pass it through the $ignoring parameter: entries passed that way are rot-checked, because a hole shaped like coverage is worse than no coverage.
ArchPresets::finalByDefault('RoundlyConsulting\Shops\Actions', [SomeBase::class]);
ArchPresets::noLocalCryptoPrimitives('RoundlyConsulting\Passkeys', ['RoundlyConsulting\Passkeys\Attestation']);
ArchPresets::noDebuggingLeftovers(['RoundlyConsulting\Shops\Debug\Inspector']);An entry fails when:
- It names nothing that exists — no class, interface, trait or enum, and no namespace that holds PHP files: a typo, or an exemption that outlived the class it excused.
- It exists but matches nothing the preset scans — a real class from another namespace handed to strictTypes, finalByDefault or noLocalCryptoPrimitives, or a noDebuggingLeftovers entry matching no class declared under its $srcDir.
- For modelsGoThroughTheFacade, it exempts no violating class.
Not detected: an entry naming a class that already complies, such as an already-final class in a finalByDefault list. It is inert today and silently re-opens the ban the day that class stops complying — keep each list to the classes that need it.
The parameter, not ->ignoring()
The three presets built on Pest’s arch layer return the underlying arch expectation, so Pest’s fluent ->ignoring() still composes — but it is not checked:
// Checked: a stale or misspelled entry FAILS.
ArchPresets::finalByDefault('RoundlyConsulting\Shops\Actions', ['RoundlyConsulting\Nope']);
// NOT checked: the identical bogus entry passes green, silently.
ArchPresets::finalByDefault('RoundlyConsulting\Shops\Actions')->ignoring('RoundlyConsulting\Nope');The gap is stated rather than patched: ->ignoring() is Pest’s own method on an internal object whose destructor evaluates the expectation, and wedging this package between the two risks an arch case that silently stops running. Exemptions are also scoped to a class, not a function — exempting a class to permit one call relaxes the whole ban for that class, so scope it to the smallest class that needs it.
Several lists per file
Each preset registers its exemption pin under its own description, so one arch file can carry as many lists as it has presets:
ArchPresets::finalByDefault('RoundlyConsulting\Shops', [ShopException::class]);
ArchPresets::noDebuggingLeftovers([Resource::class]); // a second list — fineExemptions match by prefix
Pest excludes a class when its fully-qualified name starts with the exemption string — a prefix test, not class identity. Exempting one class therefore silently exempts every class whose name starts with the same characters:
ArchPresets::finalByDefault('RoundlyConsulting\Purchases', [Stripe::class]);
// …also silences StripeClient — without the shadow re-check, removing its final would stay green.finalByDefault closes this when you pass $ignoring: it re-checks, by reflection, every class your exemptions silence without naming. A shadowed class that is already final stays green; one that isn’t goes red, naming the class and the exemption that hid it:
These classes are not final, and `finalByDefault` cannot see them:
- RoundlyConsulting\Metrics\MetricsManager (hidden by the exemption RoundlyConsulting\Metrics\Metrics)If a shadowed class is genuinely meant to stay open, name it in $ignoring — it becomes an explicit, reviewable decision. Namespace exemptions are left alone, since excluding a subtree is deliberate. The recovery rides on the parameter; if you must use the fluent form, bind the list once and pass it to shadowedClassesAreFinal() yourself:
$ignoring = [Github::class, Batch::class];
ArchPresets::finalByDefault('RoundlyConsulting\Git')->ignoring($ignoring);
ArchPresets::shadowedClassesAreFinal('RoundlyConsulting\Git', $ignoring); // fluent form onlyBespoke rules
For an arch rule of your own, call exemptionsExist() directly. $for names the rule — it is what a developer reads when the pin fires, and it keeps two pins in one file distinct. Pass the namespace your rule scans as $within, and each entry must also match something under it:
ArchPresets::exemptionsExist($ignoring, 'no facades outside the facade layer');Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.