guanguans/rector-rules
A collection of custom Rector rules for PHP and Laravel projects that automates code refactoring, modernization, and upgrades. Register the rule sets in your rector.php config to enforce consistent standards and streamline migrations with less manual work.
guanguans/rector-rules is a dev-only Rector extension — it does not ship runtime code. Treat it like PHPStan/Rector itself: install in require-dev, wire it into rector.php, and run it over your codebase.
Minimal setup
composer require --dev guanguans/rector-rules rector/rector
Then publish/point at a config (the package exposes rule sets you can ->withPaths() on) and create or extend rector.php:
use Rector\Config\RectorConfig;
use Guanguans\RectorRules\Set\...; // set class from `vendor/guanguans/rector-rules/src/Set`
return RectorConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/src', __DIR__ . '/tests'])
->withSets([
// package set(s) you want
]);
Where to look first
vendor/guanguans/rector-rules/src/ — the rule classes are the documentation. Each rule's getNodeTypes() + refactor() tells you exactly what it rewrites.Set/ directory — prefer registering a set over hand-listing individual rules; sets are the package's supported surface.vendor/guanguans/rector-rules/README.md and the repo's own rector.php — the package dogfoods its rules, so its config is a working example.First use case
A safe, high-value entry point: run Rector in dry-run before you trust it.
vendor/bin/rector process --dry-run
Start with one rule on one directory (e.g. a single rule against app/), review the diff, commit, then widen. The 1.9.2 addition — RenameStaticMethodRector, aimed at Str UUID handling — is a good example of the kind of rule you introduce deliberately: it renames static calls such as Str::uuid()/Str::orderedUuid() to their newer equivalents (e.g. Str::uuid7()), so you want it scoped and reviewed rather than dropped into a global set on a 200k-line codebase.
Pattern 1 — Dry-run → diff → commit loop (default workflow)
Never run this in the same process as a composer update. Do it as its own committed change:
vendor/bin/rector process --dry-run --clear-cache
vendor/bin/rector process
git diff # review before committing
--clear-cache matters when you change the config between runs; Rector caches aggressively and will silently keep old results.
Pattern 2 — Composer scripts per rule-set
Give each migration an explicit, named handle so future-you knows why code looks the way it does:
"scripts": {
"refactor": "rector process",
"refactor:dry": "rector process --dry-run",
"refactor:str-uuid": "rector process app/ --only='Guanguans\\RectorRules\\Rector\\RenameStaticMethodRector'"
}
--only / --rules per invocation is the cheapest way to isolate a single rule (new or otherwise) without editing rector.php and re-caching.
Pattern 3 — Layer rules by target path
Keep framework-facing code and your own code under different rule sets. rector.php supports multiple withPaths()/withSets() calls, so:
app/ → conservative set (renames, dead-code-adjacent rules only)database/, config/ → exclude UUID-related rewrites entirely; schema/config strings are often intentionally literaltests/ → run everything, then fix fixtures by handPattern 4 — CI gate, not CI auto-fix
Add rector process --dry-run as a required job after the initial migration is merged. This catches regressions where a developer reintroduces an old Str::uuid() call. Do not let CI auto-commit Rector output — reviewed refactors only.
Pattern 5 — Pair renames with the Laravel version bump
RenameStaticMethodRector for Str UUIDs is only correct if your laravel/framework constraint actually provides the target method. Sequence it as: bump laravel/framework → run the rename rule → run the test suite. Reversing the order produces calls to methods that don't exist yet and a fatal error that looks like a Rector bug.
Pattern 6 — Wrap third-party/vendored code out of scope
Always exclude vendor/, storage/, bootstrap/cache/, and any generated stubs (_ide_helper.php, *.ide_helper.models.php). Rector will happily "fix" generated files that get overwritten on the next composer install.
Static-method rules only see static calls. RenameStaticMethodRector matches Str::uuid() AST nodes. It will not touch:
'Illuminate\Support\Str::uuid' — string callables[Str::class, 'uuid'] — array callablescall_user_func([Str::class, 'uuid'])$class::uuid() where $class is a variableStr::uuid() aliased behind your own wrapper classGrep for the method name as a string before declaring the migration done.
Also check you're not fighting an existing alias. If your codebase imports Str as something other than Illuminate\Support\Str, or uses use Illuminate\Support\Str; with a different FQCN, verify the rule's matching is FQCN-based. If a change isn't applied, inspect the use statements first — not the rule.
Rector cache invalidation is the #1 "the rule does nothing" cause. Rule added but no diff? In order: --clear-cache, confirm the rule is actually in your withSets()/withRules() (a set may not include the newest rule), confirm the file is under withPaths(), confirm it isn't matched by withSkip().
Order of rules matters. Renames that change the class being called (A::foo() → B::foo()) followed by a rule that then operates on B will run in registration order. If a second rule "misses" a call the first rule just created, run Rector twice (it converges) or reorder registrations.
--dry-run exit codes are CI-hostile by default. Check your Rector version's flag for failing on diffs; otherwise your "gate" passes green while printing diffs. Verify the job actually goes red on a deliberate violation.
Pin the version. This is a code-mutating dev dependency on a fast-moving 1.x. Use a caret constraint but commit composer.lock, and treat minor bumps as review-worthy — a rule's rewrite semantics changing between 1.9.1 and 1.9.3 can silently alter a large diff.
Extension points. Custom rules are a single class implementing Rector\Contract\Rector\RectorInterface (typically extending AbstractRector). If the package's Str rule doesn't cover your wrapper, write a thin rule that delegates — call the package rule from your own test suite to keep behaviour aligned. Register personal rules under a local namespace, not in vendor/-adjacent set files, so composer update doesn't clobber them.
Verify with tests, not with the diff. A rename that compiles can still break behaviour if the old method had subtly different output (UUID v4 vs v7 ordering is a real behavioural difference — v7 is time-sortable). Anywhere you assert on ID format, sort order, or DB index locality, re-read the assertion after the rename.
Debugging a rule that misfires: use --debug and Rector's --output-format=json to see file/line attribution, then reduce to a minimal fixture file and run --only on it. If it reproduces, you have a bug report; if not, it's a config/path problem.
How can I help you explore Laravel packages today?