PHPackages                             hihaho/rector-rules - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. hihaho/rector-rules

ActiveRector-extension[Utility &amp; Helpers](/categories/utility)

hihaho/rector-rules
===================

Hihaho Rector rules for auto-fixing code conventions

0.16.1(3w ago)111.6k↑81.9%[1 PRs](https://github.com/hihaho/rector-rules/pulls)MITPHPPHP ^8.3CI passing

Since Apr 12Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/hihaho/rector-rules)[ Packagist](https://packagist.org/packages/hihaho/rector-rules)[ Docs](https://guidelines.hihaho.com)[ RSS](/packages/hihaho-rector-rules/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (44)Versions (40)Used By (0)

Hihaho Rector Rules
===================

[](#hihaho-rector-rules)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7c4df9eb37a8484947390ff32c9faecf44c2c9ca851d928aae17dccf0698f358/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f68696861686f2f726563746f722d72756c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hihaho/rector-rules)[![GitHub Tests Action Status](https://camo.githubusercontent.com/a1cbce5db40c75aac2ff2c28d2718460df6f80e49b2cfcf243de1f3300db6cf9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f68696861686f2f726563746f722d72756c65732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/hihaho/rector-rules/actions/workflows/run-tests.yml)[![GitHub PHPStan Action Status](https://camo.githubusercontent.com/2202ed084b38cce3e5b8d701ffd6d5a5499d4b99666aa8fdc278f0e0fd914506/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f68696861686f2f726563746f722d72756c65732f7068707374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/hihaho/rector-rules/actions/workflows/phpstan.yml)[![Total Downloads](https://camo.githubusercontent.com/a65a03a11fa70b3f78521c61fa195d73339286b07db390a7e2cf50d95966341f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f68696861686f2f726563746f722d72756c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hihaho/rector-rules)[![License](https://camo.githubusercontent.com/c9bb2d6336b8d64fe257a16934e052b6ae51478b35e91eaceeeee75978c70b01/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f68696861686f2f726563746f722d72756c65732e7376673f7374796c653d666c61742d737175617265)](LICENSE)

[Rector](https://getrector.com/) rules that enforce the Laravel conventions from the [Hihaho Development Guidelines](https://guidelines.hihaho.com/): 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`](https://github.com/hihaho/phpstan-rules).

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

[](#requirements)

- PHP `^8.3`
- Rector `^2.0`
- Laravel `^12` or `^13` (rules reference `Illuminate\…` classes)

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

[](#installation)

```
composer require hihaho/rector-rules --dev
```

Usage
-----

[](#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::CODE_QUALITY,
    HihahoSetList::ELOQUENT,
    HihahoSetList::NAMING,
    HihahoSetList::ROUTING,
    HihahoSetList::MIGRATIONS,
    HihahoSetList::IMPORTS,
])
```

Rule Sets
---------

[](#rule-sets)

### Code Quality (`HihahoSetList::CODE_QUALITY`)

[](#code-quality-hihahosetlistcode_quality)

General code-quality conventions.

RuleDescription`RemoveUnnecessaryNullsafeOperatorRector`Remove the nullsafe operator (`?->`) when the receiver can never be null`NativeFunctionFlagArgumentToNamedRector`Name the opaque trailing bool/null flag of well-known native functions`FirstPartyFlagArgumentToNamedRector`Name opaque bool/null flags on a first-party method, static, or constructor call`ConfigSetMethodRector`Convert the `config([...])` array-setter form to explicit `config()->set(...)``RemoveDefaultValuedArgumentRector`Drop an argument whose value equals the callee parameter's default#### `RemoveUnnecessaryNullsafeOperatorRector`

[](#removeunnecessarynullsafeoperatorrector)

```
-return $this->resource->poster?->original;
+return $this->resource->poster->original;
```

**Why?** A `?->` on a value the type system guarantees is non-null is dead noise — it reads as "this might be null" when it can't be. PHPStan already *reports* this at `level: max` under bleeding-edge (`nullsafe.neverNull`, "Using nullsafe … on non-nullable type"); this rule *fixes* it, so the two complement each other.

**Scope &amp; safety:**

- Handles both `?->property` and `?->method()`, and converts only the segment whose immediate receiver is provably non-null — so in a chain like `$resource?->maybePoster?->original` only the load-bearing nullsafe survives.
- Nullability is read from the PHPStan `Scope` (`getNativeType`), which resolves a `?Foo` receiver to `Foo|null` and leaves it untouched. A receiver that is `mixed`/unknown, a union with a scalar, or possibly-null is always left alone — the rule only removes a `?->` it can prove is redundant.
- By default it ignores phpdoc-only non-nullability (e.g. an Eloquent `@property`), so a stale annotation can't cause a wrong removal. Pass `['trust_phpdoc_types' => true]` via `withConfiguredRule()` to also trust phpdoc.

#### `NativeFunctionFlagArgumentToNamedRector` &amp; `FirstPartyFlagArgumentToNamedRector`

[](#nativefunctionflagargumenttonamedrector--firstpartyflagargumenttonamedrector)

```
-$found = in_array($needle, $haystack, true);
+$found = in_array($needle, $haystack, strict: true);
```

```
-$token = $store->resolve($platform, false);
+$token = $store->resolve($platform, inherit: false);
```

**Why?** A bare `true`/`false`/`null` at a call site is opaque — the reader has to open the callee to learn what the flag means. Naming the parameter makes the call self-documenting. The two rules split by what owns the parameter name:

- `NativeFunctionFlagArgumentToNamedRector` works off a curated map of native functions (`in_array`/`array_search` → `strict`, `json_decode` → `associative`) whose flag names are frozen by PHP. Extend or override it via `['function_flag_arguments' => ['in_array' => [2 => 'strict']]]`.
- `FirstPartyFlagArgumentToNamedRector` resolves the parameter name by reflection across method, static, and constructor (`new`) calls — including through a nullable receiver (`Foo|null`, the usual shape of a docblock-typed property). It fires only for callees in your own namespaces — never vendor signatures, whose parameter names can change under semver. Defaults to `['App\\']`; configure with `['first_party_namespaces' => ['App\\', 'Domain\\']]`.

**Scope &amp; safety:**

- Only a bare `true`, `false`, or `null` literal is named — a variable, constant, or enum case is already self-documenting and is left alone.
- A flag is named only when every argument to its right is already named or is itself a flag being named — the call's trailing "namable run". This keeps the result valid (a positional argument after a named one is a PHP fatal), so `$store->configure($key, false, isBoolean: true)` names the `false`, while `json_decode($j, true, 512)` (a positional `512` follows) is left as-is. By default the rule never names a flag that would force a *non-flag* argument to be named too.
- **Cascade (opt-in).** `FirstPartyFlagArgumentToNamedRector` accepts `['cascade_trailing_args' => true]`. With it on, a flag that is *not* last is still named by also naming the positional arguments that follow it — `$store->loadCount(true, $start, $end)` → `loadCount(hasStarted: true, start: $start, end: $end)`. Off by default because it produces broader diffs (the trailing non-flag arguments get named purely to satisfy PHP's ordering rule). Anchored on a flag, so a call with no flag is never touched.
- **Name preceding positionals (opt-in).** `['name_preceding_positionals' => true]`names the positional arguments *before* an already-named one, for calls that mix the two — `$store->paginate(1, perPage: 50)` → `paginate(page: 1, perPage: 50)`. Off by default and first-party only (naming couples to the parameter name); a call with no named argument has nothing to anchor on and is left alone.
- An already-named argument, an unpacked argument (`...$args`), a variadic target parameter, a first-class callable (`strlen(...)`, `$store->resolve(...)`), and a callee that can't be resolved (dynamic name, closure, `__call`) are all skipped.

#### `ConfigSetMethodRector`

[](#configsetmethodrector)

```
-config(['queue.default' => 'sync']);
+config()->set('queue.default', 'sync');
```

**Why?** The array form of `config()` is a setter dressed up as a getter — it reads like a lookup but mutates the repository. The explicit `config()->set(...)` form names the operation, so the call site states that it writes. A multi-key array is expanded into one `set()` call per pair, preserving source order.

**Scope &amp; safety:**

- Fires only when the `config([...])` call is the whole statement — a `config([...])`inside an assignment, condition, or other expression is left alone (multi-key expansion is only valid as standalone statements).
- Only string-literal keys are converted. A dynamic key (`config([$key => $v])`) or a class-constant key (`config([Config::KEY => $v])`) is left untouched, and an empty array is a no-op.
- Idempotent — the already-converted `config()->set(...)` form is a method call, not the `config()` function call the rule matches, so re-running changes nothing.

#### `RemoveDefaultValuedArgumentRector`

[](#removedefaultvaluedargumentrector)

```
-$user->withPosts(callback: null, times: 2);
+$user->withPosts(times: 2);
```

**Why?** Passing an argument that just repeats the parameter's default is noise — it reads as if a non-default value were chosen. Dropping it ("skip optional parameters") makes the call state only what differs from the default.

**Scope &amp; safety:**

- An argument is dropped only when the parameter's default is a single, statically known constant — `null`, a bool/int/float/string literal, an empty array `[]`, or a class constant that resolves to one of those — **and** the argument is the exact same type and value. The match is strict: `0` is not dropped against a `false` default, `''` not against `null`. A computed-expression default, or an enum-case object default (`= Status::Active`), is never droppable.
- By default it drops only an **already-named** default argument (order-independent) or a **trailing positional** default (iteratively). These are allowed on any callee, including vendor — they couple only to the default *value*.
- A positional default is **not** dropped once an *earlier optional positional* argument was overridden (passed a non-default value) — that default is often the overridden argument's operand. So `->has($relation, '=', 1)` keeps the `1` (the operator `'='`overrides the `'>='` default and would otherwise dangle), while `->has($relation, '>=', 1)` collapses to `->has($relation)`. Required arguments are never overrides; named arguments are unaffected.
- **Cascade (opt-in).** `['cascade_drop' => true]` also drops a *mid*-positional default by naming the arguments after it — `$factory->attach($user, [], $relationship)`→ `attach($user, relationship: $relationship)`. First-party only (it couples to parameter names) and gated behind `first_party_namespaces` (default `App\`).
- **Exclude specific calls (opt-in).** `['exclude_calls' => [ThrottleRequests::class => ['with']]]`tells the rule to never touch the listed methods (keyed by class FQN → method names). A call matches when the configured class is-a its *declaring* class or its *called*class — so for an inherited factory like `ThrottleRequestsWithRedis::with()` (declared on `ThrottleRequests`), configuring either the base or the subclass works. Use it for methods whose return value is serialized in an argument-count-sensitive way — e.g. a middleware factory stringified into a route signature (`ThrottleRequests::with(60, 1)`→ `throttle:60,1`), where dropping a default that equals its parameter default still changes the serialized string. The parser can't detect that coupling, so it's an explicit opt-out — finer than a per-file skip.
- An unpacked argument (`...$args`), a variadic target parameter, a first-class callable, and an unresolved callee are all skipped.

#### Opt-in: `NamedArgumentFromManifestRector` (not in any set)

[](#opt-in-namedargumentfrommanifestrector-not-in-any-set)

`FirstPartyFlagArgumentToNamedRector` names a flag only when Rector's own (bare-PHPStan) type resolution can identify the receiver. Whether a receiver resolves depends on *how* its type is declared:

Receiver type sourceRector can resolve the receiver?What to doNative docblock `@property` (e.g. `@property Foo $bar` on the class)**Yes**, already — PHPStan's built-in annotation extension reads itNothing extra — the receiver is already visible. (The rule still applies its usual gates: it only names a flag when the receiver is a *single* first-party class, so an `@property` pointing at a vendor class or a union still skips.)Extension-only dynamic property — no declared property, no typed docblock, type synthesized by a `PropertiesClassReflectionExtension` (larastan attribute synthesis, a container/service accessor, a generic-inherited property)**No**, not by defaultLoad the extension into Rector with `->withPHPStanConfigs([...])` — see belowMethod invented at runtime (a `Macroable` macro / mixin, never statically registered)**No**, and static analysis can't see it at allUse the manifest bridge below**In-engine option (extension-only properties).** Rector loads consumer PHPStan config into the same engine that powers its type resolution, so pointing it at a larastan-enabled neon lets `FirstPartyFlagArgumentToNamedRector` resolve those receivers directly — no manifest:

```
->withPHPStanConfigs([__DIR__ . '/phpstan.neon']) // must include larastan's extension
```

**Manifest bridge (`NamedArgumentFromManifestRector`).** For receivers static analysis can't resolve even with extensions loaded — chiefly runtime macros — this rule applies findings a consumer-side PHPStan pass computed, with **no type resolution of its own**. A ready-made producer ships in [`hihaho/phpstan-rules`](https://github.com/hihaho/phpstan-rules) (a Collector that writes the JSON manifest in this schema) — include its `named-argument-manifest.neon`, or write your own producer to the format below.

It is **not in any set** and is a **no-op until configured** with a manifest path:

```
->withConfiguredRule(NamedArgumentFromManifestRector::class, [
    NamedArgumentFromManifestRector::MANIFEST => __DIR__ . '/named-arguments-manifest.json',
])
```

The manifest is a JSON array of records, each naming one argument of one call:

```
[{ "file": "app/Services/TokenStore.php", "line": 42, "method": "getToken", "argIndex": 1, "paramName": "inherit", "value": "false" }]
```

- `file` — project-relative path; matched as a path-segment suffix of the file under traversal, so emit root-relative paths to keep the suffix unambiguous.
- `method` — the **method name** for a method/static call (never namespaced), or the resolved **class FQCN** for a `new` expression (both producer and Rector run name resolution, so the FQCN is the stable key there).
- `value` *(optional)* — a literal flag (`true`/`false`/`null`) the argument must still hold; a drift guard so a stale manifest line never mis-names a since-changed argument.

**Scope &amp; safety:** records are matched by `file + line + method`, applied only to positional, not-yet-named, non-unpacked arguments. A first-class callable is skipped, and a rename that would leave a positional argument after a named one (invalid PHP — because the manifest named a non-trailing argument without its trailing siblings) is refused for the whole call. The one residual ambiguity is two calls of the **same method name on the same physical line** with different receiver types and parameter names — keep one call per line (formatters already do) to avoid it.

**Manifest validation:** a malformed manifest fails loud — invalid JSON or a non-array payload throws an `InvalidArgumentException` naming the path — while a single structurally-invalid record (missing key, wrong scalar type, or an empty `file`/`method`/`paramName`) is skipped so one bad line never fails the run.

**The manifest is invisible to Rector's cache.** Rector keys each file's cache on the source content plus the configuration *parameters* — never the content of a file a rule points at. So a regenerated manifest with new findings, over unchanged source, is served from cache and **silently skipped**. Two ways to stay correct:

- **Run the pass with `rector process --no-cache`** (or `--clear-cache`). Simplest; fits the regenerate-then-apply workflow, where the manifest changes every cycle anyway.
- **Register `ManifestCacheMetaExtension`** to fold the manifest's hash into the cache key, so Rector reprocesses exactly when the manifest content changes and keeps the cache while it is stable (useful for a standing CI Rector check). Bind it with the manifest path and tag it directly — do *not* use `cacheMetaExtension()`, which re-binds the class to autowiring and drops the path:

    ```
    use Hihaho\RectorRules\Caching\ManifestCacheMetaExtension;
    use Rector\Caching\Contract\CacheMetaExtensionInterface;

    $manifest = __DIR__ . '/named-arguments-manifest.json';
    $rectorConfig->ruleWithConfiguration(NamedArgumentFromManifestRector::class, [
        NamedArgumentFromManifestRector::MANIFEST => $manifest,
    ]);
    $rectorConfig->singleton(ManifestCacheMetaExtension::class, fn () => new ManifestCacheMetaExtension($manifest));
    $rectorConfig->tag(ManifestCacheMetaExtension::class, CacheMetaExtensionInterface::class);
    ```

    This wiring needs the **classic `RectorConfig` callback style** (a `$rectorConfig`parameter). The fluent `RectorConfig::configure()->withConfiguredRule(...)` builder cannot register a tagged singleton, so put the `ManifestCacheMetaExtension``singleton()` + `tag()` in a classic-style config file (the rule itself can still be configured either way).

    Pointing at **more than one** manifest (a future manifest-driven rule, or one rule reading several manifests)? Register **one** extension and pass it **every** manifest path — `new ManifestCacheMetaExtension($manifestA, $manifestB)`. A cache-meta extension is keyed by one identifier, so a second instance would collide with the first; one instance folds all the manifests together, reprocessing when any of them changes.

> **Running under an agent wrapper?** `laravel/pao` rewrites `--error-format` (and can interfere with Rector's stream handling), so run the manifest pass with `PAO_DISABLE=1`— e.g. `PAO_DISABLE=1 vendor/bin/rector process --no-cache`.

### Eloquent (`HihahoSetList::ELOQUENT`)

[](#eloquent-hihahosetlisteloquent)

Enforces conventions for Eloquent relation usage.

RuleDescription`CollectedByAttributeRector`Replace `newCollection()` override with the `#[CollectedBy]` attribute (Laravel 11+)`NestedArrayEagerLoadingRector`Convert dot-notation eager loading to nested-array form when multiple relations share a parent`ObservedByAttributeRector`Replace `booted()` observer registration with the `#[ObservedBy]` attribute (Laravel 11+)`RelationNameToClassConstantRector`Replace string relation names with the existing class constant of the model```
-use App\Collections\ArticleCollection;
+use App\Collections\ArticleCollection;
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
 use Illuminate\Database\Eloquent\Model;

+#[CollectedBy(ArticleCollection::class)]
 class Article extends Model
 {
-    public function newCollection(array $models = []): ArticleCollection
-    {
-        return new ArticleCollection($models);
-    }
 }
```

```
-use App\Observers\ArticleObserver;
+use App\Observers\ArticleObserver;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
 use Illuminate\Database\Eloquent\Model;

+#[ObservedBy(ArticleObserver::class)]
 class Article extends Model
 {
-    protected static function booted(): void
-    {
-        static::observe(ArticleObserver::class);
-    }
 }
```

```
 $query->with([
-    Article::COMMENTS . '.' . Comment::AUTHOR,
-    Article::COMMENTS . '.' . Comment::TAGS,
+    Article::COMMENTS => [
+        Comment::AUTHOR,
+        Comment::TAGS,
+    ],
     Article::AUTHOR,
 ]);
```

```
-$article->loadMissing('relatedArticles');
+$article->loadMissing(Article::RELATED_ARTICLES);
```

**Why?** Nested-array form states the shared parent once instead of repeating it per relation, so adding or removing a child relation is a one-line diff. Class constants make relation usages findable with "find usages" and rename-safe, where string literals silently break when a relation is renamed.

**Scope:**

- `CollectedByAttributeRector` fires when `newCollection()` has exactly one statement — `return new SomeCollection($models)` — the return type matches the constructed class, and the class extends `Illuminate\Database\Eloquent\Model` directly or indirectly. The method is removed entirely; `#[CollectedBy(SomeCollection::class)]` is prepended to the class. Methods with additional logic (filtering, merging) are left untouched. The rule is idempotent: it skips if `#[CollectedBy]` is already present. Because the attribute and the method do not resolve identically, the rule skips any class where a trait or an ancestor model supplies its own `newCollection()` (including a trait method aliased to `newCollection`). It also gates on subclassability by installed Laravel version: on **Laravel 12** `#[CollectedBy]` is read from the model's own class only, so a non-`final` base would lose its collection on subtypes — there the rule converts `final` classes only. On **Laravel 13+** the attribute is resolved up the parent chain (so subclasses inherit it just as they inherited the method), and non-final models are converted too.
- `NestedArrayEagerLoadingRector` applies to `with`, `load`, `loadMissing`, and `loadCount`. It only groups when two or more entries share the same parent prefix — a single dot-notation chain without siblings stays as-is. Grouping is applied recursively, so deeper shared prefixes nest further.
- `ObservedByAttributeRector` fires when `booted()` has exactly one statement — `static::observe(SomeObserver::class)` or `self::observe(...)` — and the class extends `Illuminate\Database\Eloquent\Model` directly or indirectly. The method is removed entirely; `#[ObservedBy(SomeObserver::class)]` is prepended to the class. The rule is idempotent: it skips if `#[ObservedBy]` is already present. The observer argument must be a `::class` constant fetch — string literals are not converted.
- `RelationNameToClassConstantRector` additionally covers `relationLoaded`, `getRelation`, `setRelation`, and `unsetRelation`, on both instance calls (`$model->load(...)`) and static calls (`Model::with(...)`, `self::with(...)`). It only fires when the receiver resolves to a single concrete class **and** that class has a *public* constant whose value equals the string — it never invents constants and never references a non-public one (which would be a fatal access error). Inside the model it emits `self::`/`static::`; elsewhere the class name. Only the relation-name argument is touched — eager-load constraint callbacks are left alone. When multiple constants share the value, only a constant named like the SCREAMING\_SNAKE\_CASE form of the relation is trusted; otherwise the string is left alone. Nested relations are converted **level by level**, in both forms: a dot-notation string (`'comments.author'` → `Post::COMMENTS . '.' . Comment::AUTHOR`) and a nested array (`['comments' => ['author']]` → `[Post::COMMENTS => [Comment::AUTHOR]]`). Each segment resolves against the model the previous relation points to — found by reading that relation method's body for its Eloquent factory call (`$this->belongsTo(Comment::class)`, etc.). Conversion is all-or-nothing per path: if any segment's model or constant can't be resolved (including polymorphic `morphTo` relations), the whole string is left untouched rather than half-converted.

### Naming (`HihahoSetList::NAMING`)

[](#naming-hihahosetlistnaming)

Enforces class naming suffixes based on the parent class.

RuleDescription`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`)

[](#routing-hihahosetlistrouting)

Enforces consistent route definitions. Only applies to files under a `routes/` directory (and skips anything under `/vendor/`).

RuleDescription`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:**

- `NormalizeRoutePathRector` only rewrites `Route::get|post|put|patch|delete|any|head`. `Route::match`, `Route::redirect`, `Route::view`, and custom verbs are left untouched.
- `RouteGroupArrayToMethodsRector` only 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.

#### Opt-in: `MiddlewareStringToClassRector` (not in any set)

[](#opt-in-middlewarestringtoclassrector-not-in-any-set)

Rewrites string route-middleware references to the class-based fluent form Laravel 10.9+ provides — `Authenticate::using()`, `Authorize::using()`, `ValidateSignature::relative()`, … — so references become refactor-safe and IDE-navigable instead of magic strings:

```
-Route::middleware('auth:sanctum')->group(function () {
-    Route::get('/posts', [PostController::class, 'index'])->middleware('can:viewAny,post');
-});
+Route::middleware(\Illuminate\Auth\Middleware\Authenticate::using('sanctum'))->group(function () {
+    Route::get('/posts', [PostController::class, 'index'])->middleware(\Illuminate\Auth\Middleware\Authorize::using('viewAny', 'post'));
+});
```

Each helper returns the **same resolver string** the alias would produce, so the rewrite is behaviour-preserving. It is **not in any set** (Laravel doesn't document this form as a recommended convention) and is registered by FQN:

```
->withConfiguredRule(MiddlewareStringToClassRector::class, [
    MiddlewareStringToClassRector::CONVERT_BARE_ALIASES => true,
])
```

**Scope.** Rewrites string and array-of-string middleware references across the surfaces where they appear:

- `->middleware()` / `Route::middleware()` / `withoutMiddleware()` on a receiver that resolves to an Illuminate routing type (so an unrelated `->middleware()` method is never touched) — every argument.
- the `bootstrap/app.php` middleware configurator — `$middleware->group($name, [...])`(the group middleware, not the name), `append(...)`, and `prepend(...)`.
- Laravel 11+ controller `new Middleware('auth:sanctum')` value objects (the `HasMiddleware::middleware()` return shape).

Group names (`web`, `api`), custom/package aliases (`fortify.*`, `role:`, …), variables, and already-class-form references are left untouched. The `can:` model arguments stay string literals — never upgraded to `::class`, which would change the authorisation target.

**Default convert-set excludes `auth` and `guest`.** Converting an alias to its hardcoded *framework* class is only behaviour-preserving when the app leaves that alias at the framework default. `auth` and `guest` are the aliases apps most often remap to a custom subclass carrying real logic, so they are **not** converted by default — list them in `aliases` (after confirming they're unremapped) to opt in. The default set is `auth.basic`, `can`, `password.confirm`, `signed`, `verified`.

**Configuration.**

KeyDefaultPurpose`aliases`the five behaviour-safe aliases (`auth`/`guest` excluded)Narrow or extend which aliases are converted.`convert_bare_aliases``true`Also rewrite no-param forms (`'signed'`→`ValidateSignature::class`). Set `false` for `alias:param` forms only.`include_throttle``false`Opt into `throttle` conversion (see caveat).`throttle_class`unsetRequired when `include_throttle` is on.**Throttle caveat.** `throttle` is excluded by default. Its target class (`ThrottleRequests` vs `ThrottleRequestsWithRedis` vs a custom subclass) depends on app-global config (`->throttleWithRedis()`, or an overridden alias) that is invisible at the call site, so the rule will not guess it — converting blindly would silently switch a Redis-throttling app to the database limiter. To enable it, set `include_throttle => true` **and** the explicit `throttle_class` matching your app; then `'throttle:api'` → `{class}::using('api')` and `'throttle:60,1'` → `{class}::with(60, 1)`.

### Migrations (`HihahoSetList::MIGRATIONS`)

[](#migrations-hihahosetlistmigrations)

Enforces self-contained, production-safe migrations. Only applies to files in the `database/migrations/` directory.

RuleDescription`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(Article::COMMENTS_ENABLED)->nullable();
+    $table->string('description');
+    $table->boolean('comments_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:**

- `RemoveAfterColumnPositioningRector` only strips `->after()` calls whose receiver is a `ColumnDefinition` (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.
- `InlineMigrationConstantsRector` skips enum cases so `Status::Active` keeps its enum semantics instead of silently becoming a string literal.

#### Opt-in: `FlagColumnToBooleanRector` (not in any set)

[](#opt-in-flagcolumntobooleanrector-not-in-any-set)

Converts flag-style integer columns to `boolean` in migrations:

```
-$table->tinyInteger('is_published')->default(1);
+$table->boolean('is_published')->default(true);
```

This rule is **deliberately not in the `MIGRATIONS` set** and is a **no-op until you opt in**, because it is only safe under **MySQL/MariaDB** (where `tinyInteger` is `tinyint` and `boolean` is `tinyint(1)` — identical storage). On PostgreSQL it would be an incompatible `smallint`→`boolean` change. Register it explicitly as a one-time normalisation:

```
->withConfiguredRule(FlagColumnToBooleanRector::class, [
    FlagColumnToBooleanRector::CONFIRM_MYSQL_COMPATIBLE => true,
])
```

**Caveat — historical migrations.** Editing an already-run migration does not change production (it won't re-run); it only changes freshly-built databases, so prod stays `tinyint` while a fresh DB gets `tinyint(1)`. On MySQL this is cosmetic (display width), but treat the run as a deliberate one-time normalisation, not a routine pass.

**Scope:** only signed `tinyInteger` columns whose name matches a flag pattern (`is_`/`has_`/`should_`/`enable_`/… prefixes, `_enabled`/`_disabled`/`_required`suffixes — configurable via `name_prefixes`/`name_suffixes`) **and** that carry an explicit `->default(0|1|true|false)`. `unsignedTinyInteger` is intentionally **out of scope** — its 0-255 range would be lost if a misclassified flag-named column were narrowed to a signed `tinyint(1)` (signed `tinyInteger`→`boolean` is storage-identical, so it has no such risk). Defaultless, auto-increment, `->change()`, wider-type, and non-flag columns are left untouched. The name + default heuristic still isn't proof a column is boolean (e.g. `has_count`), so review the diff.

### Imports (`HihahoSetList::IMPORTS`)

[](#imports-hihahosetlistimports)

Configurable import aliasing to prevent ambiguous class names.

RuleDescription`AliasImportRector`Rename imports using configured aliasesDefault configuration:

- `Illuminate\Database\Eloquent\Builder` → `EloquentQueryBuilder`
- `Illuminate\Database\Query\Builder` → `QueryBuilder`
- `Illuminate\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 `use` and grouped `use Foo\{A, B};` imports
- Type hints, `new` expressions, `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

[](#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 (`HihahoSetList::TESTING`)

[](#testing-hihahosetlisttesting)

Replaces magic table-name strings in database assertions with the model class, and converts verbose single-id existence checks to their expressive equivalents.

RuleDescription`AssertDatabaseTableToModelClassRector`Replace a database-assertion table string with the matching Eloquent model class`AssertModelExistsRector`Replace `assertDatabaseHas/Missing` single-`id` checks with `assertModelExists/Missing`#### `AssertDatabaseTableToModelClassRector`

[](#assertdatabasetabletomodelclassrector)

```
-$this->assertDatabaseHas('users', ['email' => $email]);
+$this->assertDatabaseHas(User::class, ['email' => $email]);
```

**Why?** A model class is rename-safe and navigable with "find usages", where a table string silently rots when the table is renamed. Laravel resolves the table from the model, so the assertion's behaviour is unchanged.

**Scope:**

- Applies to `assertDatabaseHas`, `assertDatabaseMissing`, and `assertDatabaseCount`, called as `$this->…` or `self::…`/`static::…` inside a PHPUnit `TestCase` subclass. (`assertSoftDeleted`/`assertNotSoftDeleted` are intentionally excluded — with a model they also resolve the deleted-at column, so a table match alone doesn't prove behaviour is preserved.)
- **Verify-or-skip:** the string is rewritten only when the resolved model's own table *provably* equals it. A model with a mismatched `$table`, an overridden `getTable()`, or no resolvable model at all is left untouched — a missed conversion is acceptable, a wrong one is not.
- Configurable: `model_namespace` (default `App\Models`) and a `table_to_model` map for tables the singularise-and-studly convention can't resolve. A map entry is still verified, never trusted blindly.

#### `AssertModelExistsRector`

[](#assertmodelexistsrector)

```
-$this->assertDatabaseHas(Article::class, ['id' => $article->id]);
-$this->assertDatabaseMissing(Article::class, ['id' => $article->id]);
+$this->assertModelExists($article);
+$this->assertModelMissing($article);
```

**Why?** `assertModelExists($model)` is the idiomatic Laravel assertion for checking a model still exists in the database. The `assertDatabaseHas(Model::class, ['id' => $model->id])` form is more verbose and repeats information already carried by the model instance.

**Scope:**

- Fires when arg 1 is `SomeModel::class`, arg 2 is a single-item `['id' => $var->id]` array, and PHPStan can confirm `$var` is typed as `SomeModel`.
- Also accepts class-constant keys (`SomeModel::ID`) when the constant resolves to the string `'id'`.
- Skips calls with three arguments (the third is a connection name — dropping it would silently switch connections).
- Skips multi-key arrays (those assert attribute values, not just existence).
- Only fires inside a `PHPUnit\Framework\TestCase` subclass called on `$this` or `self::`/`static::` — non-test helpers with a same-named method are left alone.

#### Opt-in: `TestFieldStringToConstantRector` (not in any set)

[](#opt-in-testfieldstringtoconstantrector-not-in-any-set)

```
 // internal endpoint — a field rename would silently desync the test
-$this->postJson(route('orders.store'), ['id' => $order->id]);
+$this->postJson(route('orders.store'), [StoreOrderRequest::ID => $order->id]);

 // public API endpoint — the literal IS the wire contract, so keep it a string
-$this->postJson(route('api.orders.store'), [StoreOrderRequest::ID => 1]);
+$this->postJson(route('api.orders.store'), ['id' => 1]);
```

**Why?** The safe move is *asymmetric* by endpoint. In a test that exercises an *internal* endpoint, a hard-coded field-name string (`'id'`, `'player_url'`) is a refactor hazard — a field rename silently desyncs the test from its FormRequest. But in a test that exercises a *public API surface*, that same literal **is the wire contract**: a constant there lets a value-rename pass the test while silently breaking the public API. So the rule is **bidirectional** — it converts a literal key to the FormRequest constant on internal endpoints and inlines a constant key back to its literal on public ones.

**How?** The rule resolves everything itself, statically, from your **route files** — no manifest, no generator, no application boot. For each `route('name')` it reads the route file to find the controller action, reflects the action's first `FormRequest` parameter (whose first-party `string` constants name the fields), and classifies the route **internal or public** by whether a configured internal-middleware token appears in its statically-accumulated middleware stack. A route it cannot prove — unknown name, unparseable middleware, no FormRequest — is left untouched (default-safe).

It is **not in any set** and is a **no-op until configured**. Three config values:

```
->withConfiguredRule(TestFieldStringToConstantRector::class, [
    TestFieldStringToConstantRector::ROUTE_FILES => [
        __DIR__ . '/routes/web.php',
        __DIR__ . '/routes/api.php',
    ],
    TestFieldStringToConstantRector::INTERNAL_MIDDLEWARE => [
        \App\Http\Middleware\Authenticate::class,
    ],
    TestFieldStringToConstantRector::FIRST_PARTY_PREFIX => 'App\\',
])
```

- `ROUTE_FILES` — absolute paths to the route files to parse for the route → request map.
- `INTERNAL_MIDDLEWARE` — the tokens that mark a route internal: middleware FQCNs (matched as `Foo::class`) and/or string aliases (matched as `'auth'`). **The config contract:** list *every* token (FQCN or alias) that marks an endpoint internal; a token not listed is treated **public**. Get this wrong in the omitting direction and an internal route is classified public, which would inline a constant back to a literal and drop the refactor safety — so list the auth boundary for every internal area.
- `FIRST_PARTY_PREFIX` — the namespace prefix of constants eligible for rewriting, so only your own request constants are written (never a vendor constant).

**Cache correctness:** the rule derives its rewrites from your route files (and the reflected request constants), but Rector keys its per-file cache on the *test* file's content and the config *parameters* — not the route files' content. So with caching on, a route change (a new route, a moved middleware, a renamed constant) over unchanged test files would be served stale until you clear the cache. Fold the route files into the cache key the same way the manifest rules do: register `ManifestCacheMetaExtension` with your route file paths (it hashes any file paths you give it), or run the rule's pass with `rector process --no-cache`. See the cache note under `NamedArgumentFromManifestRector`above for the singleton-binding wiring.

**Correlation is same-call-site:** the rule rewrites a payload only where the verb call names its route directly — `$this->postJson(route('orders.store'), ['id' => …])`. The route name is read from a literal `route('name', …)` in argument position 0; the payload is the **first array argument** of the verb call (Laravel's `$uri, $data, $headers`signature, so a trailing headers array is never mistaken for the payload, and `route()`'s own nested params array stays inside the `route()` call). A variable URL (`post($url, …)`), a raw-string URL, or a non-literal route name is skipped.

**Boundary and coverage caveats** (hihaho's own routes hit none of these):

- The internal boundary must appear as a **direct token** in the route files — inline or on an enclosing group. If your auth middleware is pulled in by a **middleware-group expansion** (e.g. the `web` group adds it via the HTTP kernel), the static parse cannot see it; those routes look public and must be excluded from the rule's scope.
- A `Foo::using(...)` static call is treated as **public** (it is not the bare `::class`token, exactly as the booted middleware stack would see it). A **dynamic** middleware argument (a variable, concatenation, or call) makes the route's stack unknowable, so the route is **skipped**, not guessed.
- Route **actions** are resolved only when written as `Controller::class` (invokable) or `[Controller::class, 'method']`. A **string action** inside a `Route::controller(Controller::class)->group(...)` block (`Route::post('/x', 'store')`) is not resolved — the enclosing controller is not carried into the nested route — so those endpoints are a **no-op** (left untouched, never mis-rewritten). Use the array-callable form for tests you want this rule to cover.

**Scope &amp; safety:** the rule rewrites **only in array-key position** (a request-payload key), never in value position — it touches only `ArrayItem::$key`, which makes a value-position field name structurally untouchable. `to_const` matches the literal currently in source; `to_literal` reads the constant's value fresh by reflection and only inlines a first-party `string` constant of that route's request, so a constant that is no longer a string field (drift) is left alone. **Assertion arrays (`assertJson([...])`) are out of scope** — they check the *response*, whose keys are resource/response keys, not the request's FormRequest constants, so rewriting them would be unsound.

**Scope the rule to your tests.** It matches a *test* idiom — `post/put/patch(+Json)` with a `route('name')` URL and a payload array — but that shape can appear in application code too (an HTTP-client call, say). The rule does not gate on filename, so restrict its Rector **paths** to your test suite (the conventional Rector approach), e.g. register it in a config whose `->withPaths([...])` covers `tests/` only, rather than app-wide.

Covered by upstream Rector
--------------------------

[](#covered-by-upstream-rector)

Some rules in [`hihaho/phpstan-rules`](https://github.com/hihaho/phpstan-rules) have no counterpart here because the fix already ships in an upstream Rector set. Enable the upstream set instead of waiting for a rule in this package.

PHPStan ruleUpstream setNotes`onlyAllowFacadeAliasInBlade``LaravelSetList::LARAVEL_FACADE_ALIASES_TO_FULL_NAMES`Rewrites `use Route;` to `use Illuminate\Support\Facades\Route;` (and siblings)```
use RectorLaravel\Set\LaravelSetList;

->withSets([LaravelSetList::LARAVEL_FACADE_ALIASES_TO_FULL_NAMES])
```

Testing
-------

[](#testing)

```
composer test
```

Runs the full Pest suite. For the same quality gate the CI runs (Pint + Rector + PHPStan + tests), use `composer qa`.

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for recent changes.

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

[](#contributing)

Pull requests and issues are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup, fixture format, and what CI will check.

Security
--------

[](#security)

Please report security issues privately. See [SECURITY.md](SECURITY.md) for how.

Credits
-------

[](#credits)

- [Hihaho](https://github.com/hihaho)
- [All contributors](https://github.com/hihaho/rector-rules/contributors)

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance94

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.2% 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

Every ~2 days

Total

32

Last Release

24d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/31760627?v=4)[hihaho](/maintainers/hihaho)[@hihaho](https://github.com/hihaho)

---

Top Contributors

[![SanderMuller](https://avatars.githubusercontent.com/u/9074391?v=4)](https://github.com/SanderMuller "SanderMuller (131 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravelstatic analysisCode stylerectorrector-ruleshihahoconventionsrector-extension

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/hihaho-rector-rules/health.svg)

```
[![Health](https://phpackages.com/badges/hihaho-rector-rules/health.svg)](https://phpackages.com/packages/hihaho-rector-rules)
```

###  Alternatives

[mrpunyapal/rector-pest

Rector upgrade rules for Pest - refactoring and best practices for Pest testing framework

6977.7k67](/packages/mrpunyapal-rector-pest)[malukenho/docheader

A small library to check header docs

68417.4k139](/packages/malukenho-docheader)[savinmikhail/add_named_arguments_rector

Rector rule to add names to arguments for functions'/methods' calls

19100.7k5](/packages/savinmikhail-add-named-arguments-rector)

PHPackages © 2026

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