imanghafoori/php-search-replace
PHP Search Replace is a command-line tool for finding and replacing strings across files in a project. Use it to perform bulk edits, refactor code, or update repetitive text safely from the terminal, making large-scale file changes faster and easier.
This is a small, single-purpose library (token-level find & replace for PHP source). Treat the repo itself as the documentation: read README.md, then src/ and tests/ — for a 32-star package the test suite is the most reliable spec of the API surface.
composer require --dev imanghafoori/php-search-replace
Two things to confirm before writing code:
FileManipulator-style object) — the API is small, so a 5-minute read of src/ beats guessing.First use case — the one that pays for the install immediately: bulk mechanical refactors that are unsafe with sed/str_replace because the target string also appears in comments, log messages, and unrelated classes.
// Run on a clean git tree, from a throwaway branch
$files = /* your file list, e.g. from Finder */;
foreach ($files as $path) {
// illustrative — verify method names against the package's README/src
$replaced = search_replace_in_file($path, 'old_helper(', 'new_helper(');
// only write if changed, so you can diff what moved
}
Sanity loop: run it, git diff, eyeball every hunk, run php -l or your test suite, then commit. Never run it on a dirty working tree — the diff is your review artifact.
1. Rename behind a wrapper, not a regex. The value over str_replace is that strings and comments are skipped by the tokenizer. Use it for function/method renames, class renames, and namespace moves:
// before: app(OldService::class) / new OldService(...)
// after: app(NewService::class) / new NewService(...)
2. Placeholder/wildcard patterns for structural rewrites. The "smart" part is matching code shapes with placeholders rather than literal text, e.g. wrapping every call to a legacy helper in a new API while preserving arguments:
// pattern style: old_api(<?>)
// replacement: NewApi::make(<?>)
This is how you migrate a helper function into a class method across hundreds of call sites without hand-editing argument lists.
3. Wrap it in an Artisan command with a dry-run mode. The library is a primitive; the workflow you actually want is a repeatable, reviewable tool:
class RefactorCommand extends Command
{
protected $signature = 'refactor:old-to-new {--dry-run} {--path=app}';
public function handle()
{
foreach ($this->phpFiles($this->option('path')) as $path) {
$new = $this->transform(file_get_contents($path));
if ($new === file_get_contents($path)) {
continue;
}
$this->option('dry-run')
? $this->line("would change: {$path}")
: file_put_contents($path, $new);
}
}
}
Dry-run output doubles as a progress report on a large migration.
4. Feed it a filtered file list, not the whole repo. vendor/, storage/, bootstrap/cache/, generated files, and migrations should be excluded up front — post-filtering wastes time and risks touching things you must never rewrite.
5. One pattern per commit. Bulk codemods are unreviewable when stacked. Commit "rename OldService", then "wrap old_api calls", then "revert accidental matches". If something goes wrong you git revert one clean commit instead of untangling a 400-file blob.
6. Pair with the rest of your toolchain. Order of operations: search-replace → pint/php-cs-fixer (re-normalize whitespace) → phpstan/psalm (catch semantic breakage) → tests. The token rewrite will preserve odd formatting; the formatter fixes it.
7. Use it for framework upgrades. Laravel/Symfony deprecation sweeps (renamed facades, changed method signatures, Str:: migrations) are exactly this tool's sweet spot — and the reason it beats a naive grep is that it won't corrupt the deprecation notice string that mentions the old method name.
require-dev with an exact or ^x.y constraint, and expect the API to be under-documented. Budget time to read src/ — do not assume the README's examples cover your PHP version.nikic/php-parser (or php-token-analyzer) version conflicts are the most likely install-time failure, since static-analysis and code-quality tooling in require-dev often pins the same dependency. If Composer refuses, resolve with a dedicated refactor composer group or run it via a separate throwaway project pointed at your source checkout.$method = 'oldMethod'; $obj->$method() and call_user_func('old_fn', ...) are not matched — and that's often fine, but you must grep for them manually to avoid a half-finished migration.->save( will match every save() on every class. Scope your patterns with surrounding class/method context, or accept that you'll review the diff and revert the false positives.$replace re-introduces the $search token sequence (or a later rule matches your own output), you get runaway edits. Run each pattern once per pass, verify, and keep rules non-overlapping.@deprecated annotations, class references in docblocks) you need a plain text replace instead; use this tool for code, sed/manual for prose. Heredocs and nowdocs can also surprise you — check a sample file after the first run.git diff before committing, and keep the tree clean beforehand. Most of the failure modes above surface instantly as a weird diff. Pair with php -l on changed files (cheap, catches tokenizer-adjacent breakage) and your test suite.tools/ costs less than fighting an abandoned dependency chain.How can I help you explore Laravel packages today?