hihaho / rector-rules
Hihaho Rector rules for auto-fixing code conventions
Requires
- php: ^8.3
- rector/rector: ^2.4.1
- symplify/rule-doc-generator-contracts: ^11.2
Requires (Dev)
- laravel/boost: ^2.4
- laravel/pint: ^1.29
- nikic/php-parser: ^5.4
- orchestra/testbench: ^9.0||^10.11||^11.0
- pestphp/pest: ^3.0||^4.4
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5
- rector/type-perfect: ^2.0
- roave/security-advisories: dev-latest
- sandermuller/package-boost: ^0.2
- spaze/phpstan-disallowed-calls: ^4.10
- symplify/phpstan-extensions: ^12.0
- tomasvotruba/cognitive-complexity: ^1.0
- tomasvotruba/type-coverage: ^2.0
README
Rector rules that enforce the Laravel conventions from the Hihaho Development Guidelines: naming, routing, migration safety, and import aliasing.
For the static-analysis counterparts (rules that flag code but don't rewrite it), see hihaho/phpstan-rules.
Requirements
- PHP
^8.3 - Rector
^2.0 - Laravel
^11,^12, or^13(rules referenceIlluminate\…classes)
Installation
composer require hihaho/rector-rules --dev
Usage
Add the desired rule sets to your rector.php:
use Hihaho\RectorRules\Set\HihahoSetList; use Rector\Config\RectorConfig; return RectorConfig::configure() ->withSets([ HihahoSetList::ALL, // all rules ]);
Or pick individual sets:
->withSets([ HihahoSetList::NAMING, HihahoSetList::ROUTING, HihahoSetList::MIGRATIONS, HihahoSetList::IMPORTS, ])
Rule Sets
Naming (HihahoSetList::NAMING)
Enforces class naming suffixes based on the parent class.
| Rule | Description |
|---|---|
AddCommandSuffixRector |
Classes extending Command must end with Command |
AddMailSuffixRector |
Classes extending Mailable must end with Mail |
AddNotificationSuffixRector |
Classes extending Notification must end with Notification |
AddResourceSuffixRector |
JsonResource subclasses end with Resource; ResourceCollection subclasses end with ResourceCollection |
-class NotifyUsers extends Command +class NotifyUsersCommand extends Command
Why? Suffixes make artifact types obvious from the class name alone, and they prevent collisions like App\Mail\Welcome vs App\Notifications\Welcome from biting you later. IDE search and grep are a lot more useful when the convention holds.
Skipped: abstract classes, classes already suffixed correctly, and (in the Resource rule only) JsonResource subclasses whose names already end in Collection. Those look like naming mistakes; renaming them to FooCollectionResource would just bury the bug.
Routing (HihahoSetList::ROUTING)
Enforces consistent route definitions. Only applies to files under a routes/ directory (and skips anything under /vendor/).
| Rule | Description |
|---|---|
NormalizeRoutePathRector |
Strip leading/trailing slashes and collapse consecutive slashes |
RouteGroupArrayToMethodsRector |
Convert array-based route groups to fluent method chaining |
-Route::get('/about', fn () => 'about'); +Route::get('about', fn () => 'about');
-Route::group(['middleware' => 'web', 'prefix' => 'admin', 'name' => 'admin.'], function (): void { +Route::middleware('web')->prefix('admin')->name('admin.')->group(function (): void { Route::get('dashboard', fn () => 'dashboard'); });
Why? Fluent chains produce cleaner diffs than option arrays, and they're easier to extend when you tack on another middleware or prefix. Path normalization prevents duplicate routes from /foo vs foo/.
Scope:
NormalizeRoutePathRectoronly rewritesRoute::get|post|put|patch|delete|any|head.Route::match,Route::redirect,Route::view, and custom verbs are left untouched.RouteGroupArrayToMethodsRectoronly rewrites groups where every array key is in the supported set:middleware,prefix,name/as,namespace,domain,where,excluded_middleware,scope_bindings. Unknown keys, positional (no-key) arrays, and empty arrays are left as-is to avoid dropping configuration silently.
Migrations (HihahoSetList::MIGRATIONS)
Enforces self-contained, production-safe migrations. Only applies to files in the database/migrations/ directory.
| Rule | Description |
|---|---|
RemoveAfterColumnPositioningRector |
Remove ->after() column positioning calls (prevents disabling INSTANT DDL) |
InlineMigrationConstantsRector |
Inline class constants (string, int, float, bool, null). Enum cases are left alone. |
Schema::table('users', function (Blueprint $table): void {
- $table->string('description')->after('name');
- $table->boolean(Video::CALIPER_ENABLED)->nullable();
+ $table->string('description');
+ $table->boolean('caliper_enabled')->nullable();
});
Why? Migrations must be self-contained. Using ->after() can disable MySQL's INSTANT DDL optimization on large tables. Referencing model constants creates a dependency that breaks if the constant is later renamed or removed.
Scope:
RemoveAfterColumnPositioningRectoronly strips->after()calls whose receiver is aColumnDefinition(e.g.$table->string('x')->after('y')). Blueprint's two-arg scoping form ($table->after($col, Closure)) and unrelated->after()methods (e.g.Collection::after) are left alone.InlineMigrationConstantsRectorskips enum cases soStatus::Activekeeps its enum semantics instead of silently becoming a string literal.
Imports (HihahoSetList::IMPORTS)
Configurable import aliasing to prevent ambiguous class names.
| Rule | Description |
|---|---|
AliasImportRector |
Rename imports using configured aliases |
Default configuration:
Illuminate\Database\Eloquent\Builder→EloquentQueryBuilderIlluminate\Database\Query\Builder→QueryBuilderIlluminate\Database\Eloquent\Collection→EloquentCollection
-use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Builder as EloquentQueryBuilder; -public function scopeActive(Builder $query): Builder +public function scopeActive(EloquentQueryBuilder $query): EloquentQueryBuilder
All references in the file are updated, including:
- Flat
useand groupeduse Foo\{A, B};imports - Type hints,
newexpressions,extends,instanceof - PHPDoc tags (
@param,@return,@var,@method,@property,@mixin) on classes, interfaces, traits, enums, methods, properties, and functions
Why? Illuminate\Database\Eloquent\Builder and Illuminate\Database\Query\Builder share a short name, so an unaliased Builder type hint tells you nothing about which one the method expects. Consistent aliases show which Builder is in play without having to look at the use block, and they free up the short name if the app wants its own.
Collision safety: if the target alias is already used by another import in the same file (e.g. the file already has a use App\Queries\EloquentQueryBuilder;), the rule leaves that file alone rather than producing a PHP-fatal use X as Y collision. Rename the conflicting import or configure a different alias to proceed.
Custom alias configuration
Override the default aliases in your rector.php:
use Hihaho\RectorRules\Rector\Import\AliasImportRector; ->withConfiguredRule(AliasImportRector::class, [ 'Illuminate\Database\Eloquent\Builder' => 'EloquentQueryBuilder', 'Illuminate\Database\Query\Builder' => 'QueryBuilder', 'Illuminate\Database\Eloquent\Collection' => 'EloquentCollection', ])
Testing
composer test
Runs the full Pest suite. For the same quality gate the CI runs (Pint + Rector + PHPStan + tests), use composer qa.
Changelog
See CHANGELOG.md for recent changes.
Contributing
Pull requests and issues are welcome. See CONTRIBUTING.md for the local setup, fixture format, and what CI will check.
Security
Please report security issues privately. See SECURITY.md for how.
Credits
License
MIT. See LICENSE.