Search by

zero-to-prod / laravel-rector

Opinionated Rector Rules for Laravel

Maintainers

Package info

github.com/zero-to-prod/laravel-rector

pkg:composer/zero-to-prod/laravel-rector

Transparency log

Statistics

Installs: 429

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.5.0 2026-09-03 22:46 UTC

This package is auto-updated.

Last update: 2026-09-03 22:46:57 UTC


README

Opinionated Rector Rules for Laravel

Requirements

Installation

composer require zero-to-prod/laravel-rector

Configuration

CLI install. It asks for every value the package can be configured with and writes config/laravel-rector.php:

php artisan laravel-rector:install

Rerunning it is safe: the file reports created, unchanged or updated, and is only overwritten once you confirm.

To publish the configuration file by itself instead:

php artisan vendor:publish --tag=laravel-rector-config

Rules

Register the rules you want in rector.php:

use Rector\Config\RectorConfig;
use ZeroToProd\LaravelRector\Rector\AddReadonlyToClassWithTraitRector;
use ZeroToProd\LaravelRector\Rector\AddTypeToConstOnReadonlyClassRector;
use ZeroToProd\LaravelRector\Rector\CollapseSingleLineDocblockRector;
use ZeroToProd\LaravelRector\Rector\EnforceControllerSuffixRector;
use ZeroToProd\LaravelRector\Rector\EnforceInvokableControllerRector;
use ZeroToProd\LaravelRector\Rector\EnforceInvokableControllerRouteRector;
use ZeroToProd\LaravelRector\Rector\EnforceRegisteredClassRector;
use ZeroToProd\LaravelRector\Rector\ForbidAttributedClassDependencyRector;
use ZeroToProd\LaravelRector\Rector\ForbidBladeAttributeValueRector;
use ZeroToProd\LaravelRector\Rector\ForbidClassDependencyRector;
use ZeroToProd\LaravelRector\Rector\ForbidClassUsageRector;
use ZeroToProd\LaravelRector\Rector\ForbidCommentPhraseRector;
use ZeroToProd\LaravelRector\Rector\ForbidDuplicateBladeElementRector;
use ZeroToProd\LaravelRector\Rector\ForbidKeywordUsageRector;
use ZeroToProd\LaravelRector\Rector\ForbidNamespaceDependencyRector;
use ZeroToProd\LaravelRector\Rector\RenameParamToMatchTypeExactCaseRector;

return RectorConfig::configure()
    ->withPaths([
        __DIR__.'/app',
        __DIR__.'/routes',
        __DIR__.'/tests',
    ])
    ->withRules([
        AddReadonlyToClassWithTraitRector::class,
        AddTypeToConstOnReadonlyClassRector::class,
        CollapseSingleLineDocblockRector::class,
        EnforceControllerSuffixRector::class,
        EnforceInvokableControllerRector::class,
        EnforceInvokableControllerRouteRector::class,
        EnforceRegisteredClassRector::class,
        ForbidAttributedClassDependencyRector::class,
        ForbidBladeAttributeValueRector::class,
        ForbidClassDependencyRector::class,
        ForbidClassUsageRector::class,
        ForbidCommentPhraseRector::class,
        ForbidDuplicateBladeElementRector::class,
        ForbidKeywordUsageRector::class,
        ForbidNamespaceDependencyRector::class,
        RenameParamToMatchTypeExactCaseRector::class,
    ]);

AddReadonlyToClassWithTraitRector

A trait can say what a class is. A class using App\Helpers\DataModel is a data model: it is handed its values and changes none of them, so it is declared readonly.

Which traits say so is yours to name, with traits. A class using one of them and not declared readonly is declared readonly, and a class using none of them is left alone. The trait has to be used by the class itself: a trait reached through another trait or through a parent is not written in the file being read.

A class PHP would refuse to declare readonly is left alone rather than broken: one declaring a property that is static, untyped, or given a default, and one that is abstract or extends another class, where the classes either side of it decide too.

Configured with:

->withConfiguredRule(AddReadonlyToClassWithTraitRector::class, [
    'traits' => [
        'App\\Helpers\\DataModel',
    ],
])
-class User
+readonly class User
 {
     use DataModel;

     public string $name;
 }

Configured with:

->withConfiguredRule(AddReadonlyToClassWithTraitRector::class, [
    'traits' => [
        'App\\Helpers\\DataModel',
    ],
    'leave_todo' => true,
])
+// TODO: declare this class readonly: it uses App\Helpers\DataModel
 class User
 {
     use DataModel;

     public string $name;
 }

AddTypeToConstOnReadonlyClassRector

Constants on a readonly class carry a type, whether the class is final or not.

A constant a parent already declares is left alone: the type it is given there is the one that counts.

 readonly class SomeModel
 {
-    public const name = 'name';
+    public const string name = 'name';
 }

Configured with:

->withConfiguredRule(AddTypeToConstOnReadonlyClassRector::class, [
    'leave_todo' => true,
])
 readonly class SomeModel
 {
+    // TODO: type this constant as string
     public const name = 'name';
 }

CollapseSingleLineDocblockRector

A docblock saying one thing is written on one line, so the three lines it used to take become the one line it needs.

A docblock saying more than one thing is left as it is written: the moment a second line carries anything at all, the shape of the block is the reader's own, and collapsing it would be a rewrite rather than a tidy.

The line is the docblock's only content, whichever line it was written on, so both a block opening on its own line and one opening on the content's line collapse the same way.

-/**
- * @throws ReflectionException
- */
+/** @throws ReflectionException */
 public function handle(): void

Configured with:

->withConfiguredRule(CollapseSingleLineDocblockRector::class, [
    'leave_todo' => true,
])
 /**
  * @throws ReflectionException
  */
+// TODO: write this docblock on one line
 public function handle(): void

EnforceControllerSuffixRector

A controller says so in its name: the class a route maps to ends in Controller, so the class behind GET /user is UserShowController.

The application's own routes decide what a controller is. The rule asks the router what every registered route maps to, booting the application to do it, so a class is held to the convention because a request reaches it rather than because of where it is filed. A class no route maps to is left alone, and so is a route mapping to a closure: it names no class to hold to anything.

Renaming a class moves every reference to it — the route, the tests, the container bindings — and none of them is in the file that declares it, so there is nothing here to rewrite. The class is reported as an error naming the file and line, and the rename is yours to make.

The application is booted from the directory Rector was run in, which is the application root. Configured with base_path, it is booted from there instead.

-// Route::get('/user', UserShow::class);
+// Route::get('/user', UserShowController::class);

-readonly class UserShow
+readonly class UserShowController
 {
     public function __invoke(User $User): View
     {
         return view('user.show', ['user' => $User]);
     }
 }

Configured with:

->withConfiguredRule(EnforceControllerSuffixRector::class, [
    'leave_todo' => true,
])
 // Route::get('/user', UserShow::class);

+// TODO: Class "UserShow" is the controller for route "GET /user" and does not end in Controller. Rename it UserShowController.
 readonly class UserShow
 {
     public function __invoke(User $User): View
     {
         return view('user.show', ['user' => $User]);
     }
 }

EnforceInvokableControllerRector

A controller is one readonly action: it declares __invoke, nothing else public, and nothing about itself it can change.

A class whose name ends in Controller is held to it. Every other public method is an action hiding in a class that already has one, and there is nothing to rewrite it to — where it belongs is a controller of its own, named for what it does. So each one is reported as an error naming the file and line, as is a controller declaring no public __invoke at all, and one not declared readonly: an action holds the dependencies it was handed and changes nothing about itself between being constructed and being called.

A constructor, a static middleware() declared for Laravel's HasMiddleware, and any method that is not public are left alone: none of them is reachable as a route action. An abstract class is left alone too — a base controller routes to nothing.

Configured with require_readonly set to false, how a controller is declared stops being the rule's business and only the invokable half is enforced.

-class UserController
+readonly class UserShowController
 {
-    public function show(User $User): View
+    public function __invoke(User $User): View
     {
         return view('user.show', ['user' => $User]);
     }
 }

Configured with:

->withConfiguredRule(EnforceInvokableControllerRector::class, [
    'leave_todo' => true,
])
 readonly class UserController
 {
     public function __invoke(): View
     {
         return view('user.index');
     }

+    // TODO: Controller declares public method "show". Controllers are invokable: move it to a controller of its own, named __invoke.
     public function show(User $User): View
     {
         return view('user.show', ['user' => $User]);
     }
 }

Configured with:

->withConfiguredRule(EnforceInvokableControllerRector::class, [
    'require_readonly' => false,
])
-class UserController
+class UserShowController
 {
-    public function show(User $User): View
+    public function __invoke(User $User): View
     {
         return view('user.show', ['user' => $User]);
     }
 }

EnforceInvokableControllerRouteRector

Controllers are invokable: a route maps to a class, never to a method on one.

[Controller::class, '__invoke'] is the same route written the long way, so it is rewritten to Controller::class. Every other action that names a method — an array callable, an @ string, Route::resource(), Route::controller() — has no invokable equivalent to rewrite it to and is reported as an error instead.

-Route::get('/user', [UserShowController::class, '__invoke']);
+Route::get('/user', UserShowController::class);

Configured with:

->withConfiguredRule(EnforceInvokableControllerRouteRector::class, [
    'leave_todo' => true,
])
+// TODO: Route action names __invoke. Pass the controller class itself.
 Route::get('/user', [UserShowController::class, '__invoke']);

EnforceRegisteredClassRector

A registry is a promise that everything of a kind is named in one place: the plugins the application installs, the handlers a dispatcher reaches, the indexes a page reads. What breaks the promise is never the registry — it is the class written to be registered and left out, which compiles, tests green from its own test, and is simply never reached.

Which kinds those are is yours to name, with registries: the class registration is declared on, and what it registers — a contract the class declares with implements, a namespace it is filed under, a suffix it is named with. A class is held to being registered when every criterion given holds for it, so one criterion is a wide net and three is a narrow one, and an entry giving none is refused as the configuration is read.

A namespace is written plainly to mean itself, or with a trailing \* to mean everything below it. Names are compared as PHP compares them, without regard to case or a leading slash. What a class declares is read from the declaration, so a contract it reaches through a parent is seen only where the parent itself is what implements names.

The registry is read by reflection, which is what lets one file answer for another: its enum cases, its constants and its default property values are gathered, arrays among them flattened, and every string found is a registration. So a registry declares what it holds however it likes — a backed enum of class strings, a const array, the property a server lists its tools on — and a class named anywhere among them is registered.

There is nothing to rewrite an unregistered class to, since where it belongs on the registry is the registry's business, so every one is reported as an error naming the class, the registry and the line. Configured with leave_todo, the rule leaves that sentence as a comment on the class instead.

Configured with:

->withConfiguredRule(EnforceRegisteredClassRector::class, [
    'registries' => [
        [
            'registry' => 'App\\Plugins\\PluginIndex',
            'implements' => 'App\\Plugins\\DescribesPlugin',
            'namespace' => 'App\\Plugins\\*',
        ],
    ],
])
 enum PluginIndex: string
 {
     case adminLink = AdminLinkPlugin::class;
+    case sitemap = SitemapPlugin::class;
 }

 final readonly class SitemapPlugin implements DescribesPlugin
 {
 }

ForbidAttributedClassDependencyRector

Some kinds of class are defined by being flat. An effect that performs exactly one side effect stops being atomic the moment it calls another effect; an action that orchestrates effects stops being the single place branching lives the moment it calls another action. Both read as a convenience at the moment they are written, both type-check, and neither is caught by anything but review.

Which kinds those are is yours to name, with attributes: a list of the attributes that mark a class as one of them. A class carrying a configured attribute must not name another class carrying that same attribute. The check is pairwise and per attribute, never across two of them, so an #[Action] naming an #[Effect] is the direction the layering is for and is left alone. A configuration naming no attribute is refused as it is read.

Membership is the attribute, not the namespace, which is the whole reason this rule exists rather than a direction on ForbidNamespaceDependencyRector: an effect, the effects it may not name and the #[Effect] attribute marking all of them are filed under one namespace, so forbidding that namespace itself would forbid every effect from naming the attribute that makes it one. Here the attribute class is simply never a match — what marks a class #[Effect] is not itself marked #[Effect].

What a class declares is read from the declaration, so an attribute it inherits from a parent does not make it one of the kind. The declaration a statement is written in is the one it is read against, and a statement written before any of them — an import — is read against the first declaration the file carries one on. What a named class declares is read by reflection, so a class the analysis cannot resolve — unautoloadable, or not there at all — carries nothing and is left alone. Only a file whose class carries a configured attribute is reflected against at all.

A statement names a class however PHP lets it: an import, a parent, an interface, an attribute, a type, a new, a static call. The name is read as resolved, so the short name an import brought in and the fully qualified one are the same class. A class naming itself — self, static, or its own name written out — names nothing new.

There is nothing to rewrite a forbidden dependency to — the fix is to hoist the call into the layer that orchestrates, which is a decision — so every statement naming one is reported as an error naming both classes, the attribute they share and the line. Configured with leave_todo, the rule leaves that sentence as a comment where it found it instead.

Configured with:

->withConfiguredRule(ForbidAttributedClassDependencyRector::class, [
    'attributes' => [
        'App\\Domain\\Effect\\Effect',
        'App\\Domain\\Action\\Action',
    ],
])
 namespace App\Domain\Effect;

 #[Effect]
 final readonly class ChargeCard
 {
     public function __invoke(Order $Order): void
     {
-        new SendReceipt()($Order);
     }
 }

ForbidBladeAttributeValueRector

An attribute value written by hand is a value a refactoring cannot follow: a path typed into an href, a route spelled out in a form's action, each of them a link still pointing where the application no longer answers.

Which values those are is yours to name, with attributes: a pattern the value must not match, keyed by the attribute it is forbidden in. A pattern PCRE cannot compile is refused as the rule is configured, naming the reason, rather than quietly matching nothing.

An attribute is matched the way HTML reads its name: without regard to case, and only where the whole name is written, so href is not found in data-href, in :href or in x-bind:href — an attribute bound to an expression is already an expression. The value is read however it is written, in double quotes, in single quotes or in neither, and the pattern is matched against the value alone.

Only Blade templates are read, the files named *.blade.php. A value written inside a Blade comment or an HTML comment is not written on the page, so it is not read.

There is nothing to rewrite a forbidden value to, so a template writing one is reported as an error naming the attribute, the value, the pattern that forbids it and the line it is written on.

Configured with leave_todo, the rule reports nothing: a template renders what it says, and a note left in one is a note the page would carry.

Configured with:

->withConfiguredRule(ForbidBladeAttributeValueRector::class, [
    'attributes' => [
        'href' => '#^/#',
    ],
])
-<a href="/home">Home</a>
+<a href="{{ route('home') }}">Home</a>

ForbidClassDependencyRector

A mechanism a project has centralized — reflection, direct file access, a raw HTTP client — is a mechanism only the code that owns it should reach for. Everywhere else, reaching for it directly is a shortcut around whatever the owner enforces on the way: a plugin doing its own reflection instead of asking the host that already swept it, a domain object opening a file instead of asking the disk abstraction the rest of the codebase agreed on. It type-checks and it works, which is exactly why nothing but a rule catches it.

This is ForbidNamespaceDependencyRector's sibling, forbidding by the same shape and the same dependencies/except configuration, but at a different granularity: there, a forbidden target is a namespace, and a used class violates it by where it is declared. Here, a forbidden target is the class itself — named exactly, or by a trailing * naming a family that shares no namespace to forbid instead. PHP's own reflection classes are the case this exists for: ReflectionClass, ReflectionMethod, ReflectionEnumBackedCase and the rest are all declared globally, so no namespace pattern reaches them, and Reflection* does — matched, like every pattern here, against the used class's own full, resolved name.

Read ForbidNamespaceDependencyRector for what dependencies and except mean, how a statement is found to name a class, and what happens when it does. The one difference is this: there, App\Http\* is read against a used class's namespace; here, the identical pattern is read against the used class's full name instead — which still means "every class under App\Http," since every one of them has a full name starting with App\Http\, and additionally lets a bare prefix with no namespace separator, Reflection*, reach a family that was never inside a namespace to begin with.

Configured with:

->withConfiguredRule(ForbidClassDependencyRector::class, [
    'dependencies' => [
        'App\\Plugins\\*' => [
            'Reflection*',
        ],
    ],
    'except' => [
        'App\\Plugins\\RouteTags\\LegacyBridge',
    ],
])
 namespace App\Plugins\AdminLink;

 final class AdminLinkPlugin
 {
-    public function tagged(string $enum, string $case): array
+    public function tagged(TaggedRoute $TaggedRoute): array
     {
-        return new ReflectionEnumBackedCase($enum, $case)->getAttributes();
+        return $TaggedRoute->attributes;
     }
 }

ForbidClassUsageRector

A class a project has decided against is a class no file should name: a facade it is moving off, a helper a rewrite replaced, a package class it no longer wants reached directly.

Which classes those are is yours to name, with classes. There is nothing to rewrite a forbidden class to, so the rule never changes the code: every statement naming one carries a comment saying so instead, and running twice leaves one comment rather than two.

A statement names a class however PHP lets it: an import, a parent, an interface, an attribute, a type, a new, a static call. The name is read as resolved, so the short name an import brought in and the fully qualified one are the same class. A statement nested in another waits its own turn, so the comment lands on the line the name is written on.

Configured with leave_todo, nothing changes: the comment is all this rule ever leaves.

Configured with:

->withConfiguredRule(ForbidClassUsageRector::class, [
    'classes' => [
        'Illuminate\\Support\\Facades\\DB',
    ],
])
+// TODO: do not use Illuminate\Support\Facades\DB
 $user = DB::table('users')->first();

ForbidCommentPhraseRector

A comment a project has decided against is a comment no file should carry: a note left for nobody, a slur, a ticket number the tracker no longer knows, a name a rewrite retired.

Which phrases those are is yours to name, with phrases. A phrase written as a delimited pattern, such as /fixme/i, is matched as a regular expression; every other phrase is matched as text, without regard to case. A pattern PCRE cannot compile is refused as the rule is configured, naming the reason, rather than quietly matching nothing.

There is nothing to rewrite a phrase to, so every comment carrying one is reported as an error naming the phrase, the comment and the line it is written on.

The phrases are read from the file's comment tokens, in a line comment, a hash comment or a docblock, so one written inside a string or a heredoc is not a violation.

Configured with leave_todo, the rule reports nothing at all: the note it would leave is the comment it just found.

Configured with:

->withConfiguredRule(ForbidCommentPhraseRector::class, [
    'phrases' => [
        '/fixme/i',
    ],
])
-// FIXME the empty case
-return $items[0];
+return $items[0] ?? null;

ForbidDuplicateBladeElementRector

An element a page is allowed one of is an element a template must write once: a second <title>, a second <h1>, a second <x-layout>, each of them a page saying two things where the browser reads one.

Which elements those are is yours to name, with elements. A name is written as the tag is, title or x-layout, and is matched the way HTML reads a tag name: without regard to case, and only where the whole name is written, so <title> is not found in <titlebar>.

Only Blade templates are read, the files named *.blade.php, and only their opening tags count: a closing tag is the same element, written again. An element written inside a Blade comment or an HTML comment is not written on the page, so it is not counted.

There is nothing to rewrite a second element to, so a template writing one is reported as an error naming the element, the number of times it is written and the lines it is written on.

Configured with leave_todo, the rule reports nothing: a template renders what it says, and a note left in one is a note the page would carry.

Configured with:

->withConfiguredRule(ForbidDuplicateBladeElementRector::class, [
    'elements' => [
        'title',
    ],
])
-<title>@yield('title')</title>
-<title>Dashboard</title>
+<title>@yield('title', 'Dashboard')</title>

ForbidKeywordUsageRector

A PHP keyword a project has decided against is a language construct no file should use: a switch whose cases should be behavior, a match whose branches should be named decisions.

Which keywords those are is yours to name, with keywords. They are matched the way PHP reads them, without regard to case, and only as PHP tokens, so a word in a comment, string, heredoc or identifier is not a violation. A configured word PHP does not reserve as a keyword is refused rather than quietly matching nothing.

There is no generally correct rewrite for a forbidden language construct, so every use is reported as an error naming the keyword and the line it is written on.

Configured with leave_todo, the rule leaves a comment on the closest statement carrying the keyword instead of reporting an error. Set todo_comment to replace the complete comment, including its TODO keyword. Running twice leaves one comment rather than two.

Configured with:

->withConfiguredRule(ForbidKeywordUsageRector::class, [
    'keywords' => [
        'switch',
    ],
])
-switch ($status) {
-    case 'paid':
-        return true;
-    default:
-        return false;
-}
+return $status === 'paid';

Configured with:

->withConfiguredRule(ForbidKeywordUsageRector::class, [
    'keywords' => [
        'match',
    ],
    'leave_todo' => true,
    'todo_comment' => '// FIXME: replace this match expression',
])
+// FIXME: replace this match expression
 return match ($status) {
     'paid' => true,
     default => false,
 };

ForbidNamespaceDependencyRector

A dependency has a direction, and the direction is what a layer, a module or a package is: a domain does not reach for the delivery mechanism that calls it, a host declaring a contract does not know what implements it, and a package's root does not name the directories filed beneath it. PHP says none of that, and every one of those inversions type-checks, passes its tests, and reads as a convenience at the moment it is written.

Which directions those are is yours to name, with dependencies: a namespace, keyed to the namespaces it must not name. except names the classes no direction applies to, which is what a registry is — the one place allowed to know both sides.

A name is written plainly, App\Domain, to mean itself, or with a trailing \*, App\Domain\*, to mean everything below it. What "below" counts is what the name is read against: a direction is read against the namespace a file declares, so App\Domain\* is every namespace under it and not App\Domain itself — which is how a package's root is forbidden its own children while the files sharing that root go on naming each other. An exception is read against a class, so App\Plugins\* is every class under App\Plugins. Names are compared as PHP compares them, without regard to case or a leading slash.

A statement names a namespace however PHP lets it: an import, a parent, an interface, an attribute, a type, a new, a static call. The name is read as resolved, so the short name an import brought in and the fully qualified one are the same class. A statement nested in another waits its own turn, so the violation is reported against the line the name is written on.

There is nothing to rewrite a forbidden dependency to — the fix is an interface, an inversion or a registry, and which of the three is a decision — so every statement naming one is reported as an error naming both namespaces and the line. Configured with leave_todo, the rule leaves that sentence as a comment where it found it instead.

A forbidden target here is a namespace: the check is what the used class's own namespace matches. Forbidding a specific class, or a family of classes sharing a name rather than a namespace — PHP's own Reflection*, all declared globally — is ForbidClassDependencyRector.

Configured with:

->withConfiguredRule(ForbidNamespaceDependencyRector::class, [
    'dependencies' => [
        'App\\Domain\\*' => [
            'App\\Http\\*',
        ],
    ],
    'except' => [
        'App\\Domain\\Registry',
    ],
])
 namespace App\Domain\Billing;

-use App\Http\Controllers\InvoiceController;
-
 final class Invoice
 {
-    public function url(): string
+    public function url(GeneratesInvoiceUrls $GeneratesInvoiceUrls): string
     {
-        return InvoiceController::urlFor($this);
+        return $GeneratesInvoiceUrls->for($this);
     }
 }

RenameParamToMatchTypeExactCaseRector

A parameter typed with a class is named after that class, in the class's own casing.

Methods that override a parent or interface declaration are left alone: their parameter names are part of a contract this rule has no business rewriting.

 final class SomeClass
 {
-    public function run(Apple $pie)
+    public function run(Apple $Apple)
     {
-        $food = $pie;
+        $food = $Apple;
     }
 }

Configured with:

->withConfiguredRule(RenameParamToMatchTypeExactCaseRector::class, [
    'leave_todo' => true,
])
 final class SomeClass
 {
+    // TODO: rename $pie to $Apple, after its type
     public function run(Apple $pie)
     {
         $food = $pie;
     }
 }

Options

Every rule takes one option, leave_todo. Configured with it, a rule stops changing the code and stops reporting an error: it leaves a comment naming the violation where it found it, so the change stays a decision for whoever reads the file.

->withConfiguredRule(EnforceInvokableControllerRouteRector::class, [
    EnforceInvokableControllerRouteRector::LEAVE_TODO => true,
])

The comment is left on the statement the violation sits on, and running twice leaves one comment rather than two, so the option is safe to run in a loop while the todos are worked off. ForbidTodoAnnotationRector is the exception that proves the rule: configured this way it reports nothing at all, because the note it would leave is the comment it just found.

Agent development

The package registers an MCP server so coding agents can read how it is meant to be used. It requires laravel/mcp, and registers nothing without it.

composer require --dev laravel/mcp
php artisan mcp:start laravel-rector

Register it with your agent:

claude mcp add laravel-rector -- php artisan mcp:start laravel-rector

Four tools are exposed:

  • readme — this document.
  • rules — every rule the package ships: what it does, the code it rewrites and how to register it, read from the rules themselves rather than written out here. The same content as the Rules section above.
  • api — the exact signature of every public class, property and method. Anything unlisted is internal and may change in any release.
  • install — what laravel-rector:install does, without a prompt to answer. Takes enabled and handle, each defaulting to the current setting, and writes config/laravel-rector.php. A file that already says something else is left alone and reported until the call passes overwrite: true.

Point the handle somewhere else, or turn the server off, in config/laravel-rector.php:

'mcp' => [
    'enabled' => true,
    'handle' => 'laravel-rector',
],

Development

composer check   # lint, rector, phpstan, docs, 100% coverage, bc-check — mutates nothing
composer fix     # rector, pint, then the docs
composer mcp list                      # the server's tools
composer mcp call api '{}'             # call one

The Rules section above is generated from the rules themselves: their class names, class doc comments and rule definitions. Everything between the rules:start and rules:end markers is written by composer docs, and composer docs-check fails when it no longer matches. Document a rule by writing its doc comment and its getRuleDefinition(), then run composer fix.

composer check requires a coverage driver (Xdebug or pcov); without one Pest cannot satisfy the --min=100 gate.

License

MIT. See LICENSE.