PHPackages                             sinemacula/laravel-route-linter - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. sinemacula/laravel-route-linter

ActiveLibrary[HTTP &amp; Networking](/categories/http)

sinemacula/laravel-route-linter
===============================

A deterministic, opt-in artisan linter for RESTful route conventions in Laravel applications.

v1.0.0(today)00Apache-2.0PHPPHP ^8.3CI passing

Since Jun 18Pushed todayCompare

[ Source](https://github.com/sinemacula/laravel-route-linter)[ Packagist](https://packagist.org/packages/sinemacula/laravel-route-linter)[ RSS](/packages/sinemacula-laravel-route-linter/feed)WikiDiscussions master Synced today

READMEChangelog (2)Dependencies (18)Versions (2)Used By (0)

Laravel Route Linter
====================

[](#laravel-route-linter)

[![Latest Stable Version](https://camo.githubusercontent.com/eaefe804760292df6f83bb54a630580f8beedea77e12b27fc1f18978e40966bd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73696e656d6163756c612f6c61726176656c2d726f7574652d6c696e7465722e737667)](https://packagist.org/packages/sinemacula/laravel-route-linter)[![Build Status](https://github.com/sinemacula/laravel-route-linter/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sinemacula/laravel-route-linter/actions/workflows/tests.yml)[![Quality Gates](https://github.com/sinemacula/laravel-route-linter/actions/workflows/quality-gates.yml/badge.svg?branch=master)](https://github.com/sinemacula/laravel-route-linter/actions/workflows/quality-gates.yml)[![Maintainability](https://camo.githubusercontent.com/27d9b5ba03bc7d2dfd59b8be50620dd80da4ec2ff9200d53b7e766f6ba55543b/68747470733a2f2f716c74792e73682f67682f73696e656d6163756c612f70726f6a656374732f6c61726176656c2d726f7574652d6c696e7465722f6d61696e7461696e6162696c6974792e737667)](https://qlty.sh/gh/sinemacula/projects/laravel-route-linter)[![Code Coverage](https://camo.githubusercontent.com/d96a3b97421beecf08bd2b6509148f3218d041039070f7ffd5d0d38d908e84b4/68747470733a2f2f716c74792e73682f67682f73696e656d6163756c612f70726f6a656374732f6c61726176656c2d726f7574652d6c696e7465722f636f7665726167652e737667)](https://qlty.sh/gh/sinemacula/projects/laravel-route-linter)[![Total Downloads](https://camo.githubusercontent.com/323df64d3711386a2385319d661be3e3944e4766e2c3c0385708b4e8213a8b7f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73696e656d6163756c612f6c61726176656c2d726f7574652d6c696e7465722e737667)](https://packagist.org/packages/sinemacula/laravel-route-linter)

A deterministic, opt-in Artisan command that lints a Laravel application's route table against a fixed catalogue of RESTful URL conventions and route-integrity checks, and exits non-zero on error-severity violations so CI can gate on it.

It reads the live route table (`Router::getRoutes()` after a full boot) plus its own config - no model versions, no probabilistic inference - so the same routes and config always produce the same verdict. It enforces the mechanically-checkable convention subset only; it is not a proof of true RESTfulness.

How It Works
------------

[](#how-it-works)

The linter is built around a small set of ports and adapters, so the rule logic carries no framework dependency. One invocation walks the whole route table once:

1. **Source** the app-owned routes from the live router, excluding vendor routes (the same set `route:list --except-vendor` reports).
2. **Normalise** each route into a framework-free value object - its URI split into segments, its parameter names, its HTTP methods, its controller handler, and its gathered middleware.
3. **Inspect** every route with the ordered per-route rules, then run the cross-route (aggregate) rules over the whole set; each rule returns zero or more violations tagged `error` or `warning`.
4. **Suppress** any violation covered by an inline waiver or a config allowlist entry.
5. **Report** the findings in a deterministic total order and exit non-zero when any `error`-severity violation survives.

A few principles hold across the surface:

- **Opt-in and deterministic.** Nothing runs until you call `route:lint`, and the same route table plus the same config yields a byte-identical verdict on every run, independent of route-cache state.
- **Every waiver is justified and per-rule.** Waivers require a written reason and target specific rules. Unused waivers - and allowlist entries matching no live route - are surfaced as stale entries so they cannot rot (reported, but they do not gate).
- **Misconfiguration fails loud.** A malformed config value (a non-array where an array is expected, an exemption missing its reason) raises an `InvalidConfigurationException` rather than silently weakening the verdict.

Rules
-----

[](#rules)

RuleSeverityChecksR1errorNo action verb in a path segment (incl. compound / pluralised), with a RESTful-rewrite hintR2errorSegments are kebab-caseR3errorSegments are lowercaseR4errorCollection segments are plural (honours configured uncountables)R5errorNo trailing or duplicate slashesR6errorNo duplicate route name (would break `route()` URL generation)R7errorStandard HTTP methods onlyR8warningNamed routes follow `{resource}.{action}`R9warningNo HTML-only `create` / `edit` action as the final literal segment on an API surfaceR10warningRoutes matching a configured pattern declare the required middlewareR11warningResource nesting no deeper than the configured number of collection levels (default three)R12errorRoute handler (controller class / method) existsNote

Rule IDs are stable across releases - a rule keeps its ID for life, so waivers and CI gates that pin an ID stay valid on upgrade.

Installation
------------

[](#installation)

```
composer require --dev sinemacula/laravel-route-linter
```

The service provider is auto-discovered. Publish the config to tune it:

```
php artisan vendor:publish --tag=route-linter-config
```

Usage
-----

[](#usage)

```
php artisan route:lint
```

Exits non-zero when any **error-severity** violation is present (warnings are reported but do not gate). Run it as a step in CI.

Waiving a Violation
-------------------

[](#waiving-a-violation)

Every waiver requires a written reason and is **per-rule**. Unused waivers (and allowlist entries matching no live route) are surfaced as stale entries so they cannot rot - these are reported but do not gate.

**Inline (preferred) - co-located at the route:**

```
Route::patch('photos/{photo}/edit', [PhotoController::class, 'edit'])
    ->ignoreRouteLint(['R9'], 'legacy admin UI - BL-123');   // waives only R9 on this route

Route::get('legacy/getStats', LegacyStatsController::class)
    ->ignoreRouteLint([], 'frozen v1 contract - BL-200');    // [] = all rules
```

Stored in the route action (survives `route:cache`).

**Config allowlist - for routes you cannot annotate:**

```
// config/route-linter.php
'exemptions' => [
    ['match' => 'photos.edit', 'rules' => ['R9'], 'reason' => 'BL-123'],  // per-rule
    ['match' => 'legacy.*',                       'reason' => 'BL-200'],  // rules omitted = all
],
```

Tuning
------

[](#tuning)

Removing a word from `verb_denylist` is **rule tuning**, not a per-route waiver - use it for legitimate domain-noun homographs (e.g. a real `transfer` resource). This is global and needs no reason. The maximum nesting depth enforced by R11 is set with `nesting_max_depth` (default `3`).

R10 (required middleware) is opt-in and ships empty. Map an `fnmatch` URI pattern to the middleware a matching route must declare; matching is an exact token comparison, so write parameterised middleware in full:

```
// config/route-linter.php
'required_middleware' => [
    'admin/*' => ['auth', 'can:access-admin'],
    'api/*'   => ['auth:sanctum'],
],
```

Extending
---------

[](#extending)

The rule set is the product surface, and it is configurable. The `rules` key lists the rules the engine runs, in order; each is a class implementing the `Rule` contract and is resolved from the container, so rules may declare constructor dependencies. Remove a built-in by deleting its line, or append your own:

```
// config/route-linter.php
'rules' => [
    \SineMacula\RouteLinter\Rules\VerbInPathRule::class,
    // …the built-in rules…
    \App\RouteLinting\NoSnakeCaseRule::class,    // your custom rule
],
```

A custom rule receives the normalised route - its segments, brace-stripped parameter names, controller handler (`Class@method`, or `null` for closures), and gathered middleware - and the active config, and returns zero or more violations:

```
use SineMacula\RouteLinter\Contracts\Rule;
use SineMacula\RouteLinter\Dto\RuleConfig;
use SineMacula\RouteLinter\NormalisedRoute;
use SineMacula\RouteLinter\Severity;
use SineMacula\RouteLinter\Violation;

class NoSnakeCaseRule implements Rule
{
    public function id(): string { return 'APP1'; }

    public function severity(): Severity { return Severity::ERROR; }

    public function inspect(NormalisedRoute $route, RuleConfig $config): array
    {
        $offenders = array_filter($route->segments, static fn (string $s): bool => str_contains($s, '_'));

        return array_map(fn (string $s): Violation => new Violation(
            ruleId: $this->id(),
            severity: $this->severity(),
            routeIdentity: $route->identity(),
            offendingSurface: $s,
            remediationHint: null,
        ), array_values($offenders));
    }
}
```

For checks that span the whole route table rather than one route at a time - duplicate detection, table-wide invariants

- implement `AggregateRule` instead. Its `inspect(array $routes, RuleConfig $config)` receives every normalised route at once and runs in a single pass after the per-route rules. List it in the same `rules` key; the engine partitions the two kinds by contract. Attribute each violation to the offending route's `identity()` so per-rule waivers still apply.

Rule IDs must be unique across both kinds - the engine rejects a duplicate at boot. Output rendering is a port too: bind your own `LintReporter` implementation (for example, to emit JSON or SARIF for CI) to replace the default console reporter.

Determinism
-----------

[](#determinism)

The same route table plus the same config yields a byte-identical verdict on every run, independent of route-cache state. It enforces the mechanically-checkable convention subset only - it is not a proof of true RESTfulness.

Requirements
------------

[](#requirements)

- PHP ^8.3
- Laravel ^12.9

Testing
-------

[](#testing)

```
composer test                # Run the test suite in parallel using Paratest
composer test:coverage       # With clover coverage report
composer test:mutation       # Mutation-testing gate (Infection) - the enforced MSI floor
composer test:mutation:full  # Full mutation suite, no thresholds (scheduled audit run)
composer check               # Static analysis and lint checks via qlty
composer format              # Format the codebase via qlty
composer smells              # Advisory code smells (duplication, complexity)
composer bench               # Run the PHPBench benchmarks
```

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for a list of notable changes.

Contributing
------------

[](#contributing)

Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on branching, commits, code quality, and pull requests.

Security
--------

[](#security)

If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for the disclosure policy and contact details.

License
-------

[](#license)

Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 60% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6262ea965c244b0c946a2f29a94da05e30846c066a0b59399466216654c78fe6?d=identicon)[sinemacula](/maintainers/sinemacula)

---

Top Contributors

[![sinemacula-ben](https://avatars.githubusercontent.com/u/118753672?v=4)](https://github.com/sinemacula-ben "sinemacula-ben (3 commits)")[![sine-macula-releases[bot]](https://avatars.githubusercontent.com/in/4070845?v=4)](https://github.com/sine-macula-releases[bot] "sine-macula-releases[bot] (2 commits)")

---

Tags

standardslaravelrestroutinglintercisine maculaconventions

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sinemacula-laravel-route-linter/health.svg)

```
[![Health](https://phpackages.com/badges/sinemacula-laravel-route-linter/health.svg)](https://phpackages.com/packages/sinemacula-laravel-route-linter)
```

###  Alternatives

[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76518.2M115](/packages/laravel-mcp)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/boost

Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.

3.5k17.6M513](/packages/laravel-boost)[laravel/ai

The official AI SDK for Laravel.

9782.1M157](/packages/laravel-ai)[laravel/folio

Page based routing for Laravel.

603526.4k31](/packages/laravel-folio)[laravel-doctrine/orm

An integration library for Laravel and Doctrine ORM

8455.5M96](/packages/laravel-doctrine-orm)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
