Search by

dashworthy / pest-plugin-arch-rails

Andrew Leach

Architecture rules for Laravel, written as Pest expectations.

Package info

github.com/dashworthy/pest-plugin-arch-rails

pkg:composer/dashworthy/pest-plugin-arch-rails

Statistics

Installs: 168

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-10-22 17:41 UTC

This package is auto-updated.

Last update: 2026-10-29 13:48:22 UTC


README

pest-plugin-arch-rails — Pest arch rules that keep Laravel code on the rails.

pest-plugin-arch-rails

Architecture rules for Laravel, written as Pest expectations. They plug straight into pest-plugin-arch: no DSL, no selector grammar, no reporting layer of its own. Every verb composes with the arch() chains you already write.

Install

composer require --dev dashworthy/pest-plugin-arch-rails

The expectations register themselves through Composer's autoload.files. There is nothing to publish and no service provider.

Areas

The package is organised by area. Each lives in its own directory under src/, registers its own verbs, and has its own page covering usage, configuration and edge cases:

  • Messaging: queued jobs, notifications and mailables
  • Models: Eloquent table names and mass assignment
  • Http: form requests, controllers, API resources and permissions
  • Modules: directory and naming rules for modular (DDD) code
  • Coverage: every class has a test at the mirrored path

Verbs

Area Verb Passes when
Messaging toBeQueued() the class implements ShouldQueue (or ShouldQueueAfterCommit, which extends it)
Messaging toBeQueuedAfterCommit() the class implements ShouldQueueAfterCommit specifically
Messaging toBeSync() the class implements neither: dispatch is deliberately synchronous
Messaging toDeclareNotificationChannels() via() exists and returns a non-empty channel list
Messaging toUseMarkdownMailTemplates() content() names a markdown template rather than a plain view
Models toMatchTableName() an Eloquent model resolves the table its class name implies
Models toGuardMassAssignment() a model declares $fillable or $guarded, or relies on the fully guarded default
Http toAuthorizeWithGate($expectedAbility) what authorize() returns depends on a gate check, of the ability $expectedAbility derives from the class name when given
Http toUseFormRequests() every public controller action takes a form request (or no request) and does not validate inline
Http toReturnJsonResources() every public controller action declares a JsonResource subclass as its return type
Http toMatchGateAbilities($directories, $alsoChecked) (on a list of permission names) every permission is checked somewhere (a Gate call, ->can(), can: middleware or @can), and every checked ability is a permission
Modules toUseApprovedDirectories($directories, $beneath) every class in a module sits in an approved directory, and is the type that directory names
Modules toUseDirectorySuffixes($suffixes, $beneath) a class in a module's directory, however deeply nested, ends in the suffix that directory calls for
Coverage toHaveTests($map, $suffix, $skip, $exempt) every class under a mapped source directory has a test file at the mirrored position

toMatchGateAbilities() is the odd one out: it is an ordinary expectation, not an arch one. Its subject is the list of permission names rather than a selection of classes, so it reports every mismatch in one run.

src/Core holds no verbs. It is the shared base the areas build on: Rule turns an inspector (a class that returns a failure message, or null) into an arch expectation whose message can name the offending value, and LineFinder points the failure at a useful line.

Example

Give each of this package's verbs its own arch() call. Pest applies a selector such as ->classes()->extending(...) only to the first verb in a chain, so a second verb would run against every class in the namespace.

use Illuminate\Database\Eloquent\Model;

arch('every model guards mass assignment')
    ->expect('App')
    ->classes()
    ->extending(Model::class)
    ->toGuardMassAssignment();

arch('every model matches its table name')
    ->expect('App')
    ->classes()
    ->extending(Model::class)
    ->toMatchTableName();

What you are accepting

These are arch expectations, so they inherit arch's behaviour:

  • One violation per run. A rule failing across forty classes reports the first and stops.

  • @pest-arch-ignore-line suppresses any of them. Police its use with an arch test of your own if that matters to you:

    arch('no source file suppresses an architecture rule')
        ->expect('App')
        ->not->toUse('@pest-arch-ignore-line');
  • An empty layer passes. A typo'd namespace turns a rule green. Assert the layer is non-empty separately if you need that guard.

Editor and agent support

The package ships a Laravel Boost skill, enforcing-arch-rails, and a guideline. Boost discovers them at resources/boost/skills/ and resources/boost/guidelines/ for every installed package, so a consuming project picks them up by running:

php artisan boost:update

The skill explains which verb covers which case and how to scope or exempt one; the guideline is the short form injected into the project's AI guidelines.

Licence

MIT.