sinemacula/laravel-route-linter 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

sinemacula/laravel-route-linter

Composer 安装命令:

composer require sinemacula/laravel-route-linter

包简介

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

README 文档

README

Latest Stable Version Build Status Quality Gates Maintainability Code Coverage Total Downloads

A deterministic, opt-in Artisan command that lints a Laravel application's route table against a fixed catalogue of RESTful URL conventions, 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

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, and its HTTP methods.
  3. Inspect every route with the ordered rule 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

Rule Severity Checks
R1 error No action verb in a path segment (incl. compound / pluralised), with a RESTful-rewrite hint
R2 error Segments are kebab-case
R3 error Segments are lowercase
R4 error Collection segments are plural (honours configured uncountables)
R5 error No trailing or duplicate slashes
R7 error Standard HTTP methods only
R8 warning Named routes follow {resource}.{action}
R9 warning No HTML-only create / edit action as the final literal segment on an API surface
R11 warning Resource nesting no deeper than the configured number of collection levels (default three)

Note

Rule IDs R6 and R10 are intentionally reserved/retired - IDs are kept stable across releases.

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

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

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

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).

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 (including its brace-stripped parameter names) 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));
    }
}

Rule IDs must be unique - 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

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

  • PHP ^8.3
  • Laravel ^12.9

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

See CHANGELOG.md for a list of notable changes.

Contributing

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

Security

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

License

Licensed under the Apache License, Version 2.0.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: Apache-2.0
  • 更新时间: 2026-06-18

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固