Stacks

Refactor a Legacy PHP Module with Atlas Without Breaking Callers (2026)

Updated 8 min read

To refactor a legacy PHP module with Atlas, you map its public surface with the lsp tool's documentSymbol operation and run findReferences on each exported symbol to enumerate every callsite before touching a line. Atlas then pins behavior by running PHPUnit through bash and recording the green baseline, restructures with apply_patch, which anchors on context lines and fails with Failed to find context against a drifted file, and re-runs PHPUnit after each hunk. The risk in a PHP refactor is silent breakage at a callsite you did not know about, and Atlas closes that gap first.

How do I find every caller of a legacy PHP class before refactoring it?

Atlas maps a legacy PHP module's public surface with the lsp tool's documentSymbol operation, then runs findReferences on each exported symbol to enumerate every callsite. In a Composer project with PSR-4 autoloading, that is the only reliable way to see who depends on src/Legacy/OrderProcessor.php.

The risk in a PHP refactor is silent breakage at a callsite you did not know about, and PSR-4 autoloading makes those callsites easy to miss. A class is instantiated by its fully qualified name in one file, aliased with a use statement in another, and resolved from a service container in a third. Atlas closes that gap before touching anything: the lsp tool's documentSymbol operation enumerates the module's public methods, and findReferences on each one returns the caller set from the language server. Atlas indexes code by AST declarations using tree-sitter, not blind line windows, so each PHP method comes back as a declaration rather than a slice, and Atlas searches code with hybrid semantic and keyword retrieval fused by reciprocal rank fusion when you need to find callers by behavior rather than by name.

How does Atlas prove a PHP refactor did not change behavior?

Atlas pins behavior first: run the existing PHPUnit tests with bash and record the green baseline before changing anything. A legacy PHP module with 84 passing tests gives you an oracle, and refactoring without recording that baseline means you cannot tell a pre-existing failure from one you just caused.

Behavior preservation is the definition of a refactor, so Atlas establishes the oracle before it moves. Atlas runs the existing tests with PHPUnit through bash and records the green baseline, which is what makes every later run meaningful. Atlas then re-runs PHPUnit after each hunk lands, not once at the end, so a break is attributed to the specific structural change that caused it rather than to a 500-line diff. That per-hunk cadence is the difference between a five-minute fix and an afternoon of bisecting a legacy PHP module. Because bash is a real shell, Atlas runs the same PHPUnit command your CI runs, using the phpunit.xml your project already defines, so a green run locally means the same thing it means in the pipeline.

Why does Atlas use apply_patch to restructure PHP code?

Atlas restructures legacy PHP with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the file has drifted. A patch that refuses to apply is far safer than an edit that lands in roughly the right place inside a 900-line PHP class.

Legacy PHP files are long, and long files are where blind edits do damage. apply_patch anchors each hunk on the surrounding context lines it expects to find, so if src/Legacy/OrderProcessor.php has changed since the refactor was planned, because a colleague pushed or PHP-CS-Fixer reformatted it, apply_patch fails with Failed to find context instead of writing into a file it no longer understands. Atlas computes a unified diff for every file edit and surfaces it for approval before writing, so each structural change to the PHP module is reviewed as its own diff. Atlas snapshots file changes as git patches so edits can be diffed and rolled back, and every Atlas tool call is permission-gated against allow, ask, and deny rules before it runs, including the Composer commands the refactor might need.

How does Atlas track a partially refactored PHP module?

Atlas tracks the remaining callsites in a todowrite list, so a partially migrated PHP module cannot be mistaken for a finished one. A refactor with 14 callers across a Composer project becomes 14 tracked items, each closed only after PHPUnit passes for the file that changed.

A half-finished refactor that looks finished is the worst outcome, because it ships. Atlas prevents that by turning the findReferences output into a todowrite list with one entry per remaining callsite, each naming a real path such as src/Http/CheckoutController.php or src/Jobs/ReconcileOrders.php. An entry is marked completed only after PHPUnit passes for that file, not when the patch applies cleanly, which keeps the list honest about what is actually proven. Atlas fans out work to subagents that can run in the foreground or in parallel background sessions, so enumeration across a large PHP monolith can be delegated while the main session works the patch queue. Atlas reads git branches, status, and diffs, so the partial state is always visible against the branch you started from.

How do I finish a legacy PHP refactor cleanly?

Atlas finishes a legacy PHP refactor by re-running the full PHPUnit suite through bash, running PHP-CS-Fixer over the touched files so they match PSR-12, and confirming Composer autoloading still resolves the moved classes. The diff a reviewer sees should be the restructuring, not whitespace churn.

Finishing is its own step. Atlas re-runs the full PHPUnit suite one last time, because a change inside a shared trait or an abstract base class can satisfy every hunk-level test and still break a suite in a different namespace. Atlas then runs PHP-CS-Fixer over the touched files, which normalizes the restructured PHP to PSR-12 so the reviewer reads the change rather than the formatting. If classes moved between namespaces, Composer autoloading is the thing that must still resolve, and Atlas verifies that through bash rather than assuming. The documented PHP setup is exactly this shape: run atlas in a project with a composer.json, let Atlas read your namespaces, autoload config, and dependencies, then ask Atlas to add PHPUnit tests or apply PSR-12 formatting, reviewing the diff. Atlas can then stage and create the commit on your behalf.

Step by step

  1. 01Run atlas in a project with a composer.json so Atlas can read your namespaces, autoload config, and dependencies.
  2. 02Map the legacy module's public surface with the lsp tool's documentSymbol operation, listing every public method on the PHP class you intend to restructure.
  3. 03Run the lsp tool's findReferences operation on each exported symbol to enumerate every callsite, including the ones resolved through PSR-4 autoloading and service containers.
  4. 04Pin behavior first: run the existing PHPUnit tests with bash and record the green baseline before changing anything.
  5. 05Restructure with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the PHP file has drifted since the plan was made.
  6. 06Review the unified diff Atlas surfaces for each hunk before it writes into src/.
  7. 07Re-run PHPUnit with bash after each hunk lands, not once at the end, so a break is attributed to the change that caused it.
  8. 08Track the remaining callsites in a todowrite list so a partially migrated PHP module cannot be mistaken for a finished one.
  9. 09Finish with a full PHPUnit run, PHP-CS-Fixer over the touched files for PSR-12 compliance, and a Composer autoload check on any moved classes.

Frequently asked questions

how do I refactor a legacy PHP class without breaking its callers
Map the class's public surface with the lsp tool's documentSymbol operation and run findReferences on each exported symbol to enumerate every callsite first. Atlas then records a green PHPUnit baseline before changing anything, so behavior preservation is measurable rather than assumed.
can an AI agent find PHP callers that are resolved through autoloading
Atlas uses the lsp tool's findReferences operation, which asks the language server rather than guessing from text, so callers that resolve a class through PSR-4 autoloading or a service container still appear. Atlas cross-checks with grep for string-based references.
what does Failed to find context mean in apply_patch
apply_patch anchors each hunk on the surrounding context lines it expects to find, and throws Failed to find context when the PHP file has drifted, for example because a colleague pushed or PHP-CS-Fixer reformatted it. A refusal is safer than a misapplied edit.
how often should tests run during a PHP refactor
Atlas re-runs PHPUnit with bash after each hunk lands, not once at the end. In a 900-line legacy PHP class, per-hunk runs mean a break is attributed to the exact structural change that caused it instead of to the whole diff.
does Atlas work with Composer projects and PSR standards
Yes. The documented PHP setup is to run atlas in a project with a composer.json, let Atlas read your namespaces, autoload config, and dependencies, then ask Atlas to add PHPUnit tests or apply PSR-12 formatting, reviewing the diff. PHP-CS-Fixer is the formatter.
how do I know a PHP refactor is actually finished and not half done
Atlas tracks the remaining callsites in a todowrite list, and an entry is marked completed only after PHPUnit passes for that file. A partially migrated PHP module therefore cannot be mistaken for a finished one.
how do I undo a bad PHP refactor made by an AI agent
Atlas snapshots file changes as git patches so edits can be diffed and rolled back. Because each structural change is its own apply_patch with its own unified diff, you revert the one hunk that broke PHPUnit rather than the whole refactor.
will Atlas break Composer autoloading if it moves PHP classes between namespaces
Atlas verifies Composer autoloading still resolves moved classes by running the check through bash rather than assuming. Atlas reads your autoload config as part of the documented setup, so namespace moves are made against the real configuration.

Try Atlas in your terminal

The terminal-native AI coding agent. Free core, single binary.

Install Atlas

Related guides

Refactor a Legacy Module with Atlas in 2026

How to refactor a legacy module with Atlas in 2026: findReferences maps every callsite, apply_patch refuses to apply against a drifted file, and bash proves behavior.

Atlas for PHP in 2026

Atlas, the terminal-native AI coding agent, empowers PHP developers in 2026 with intelligent code understanding, secure workflows, and direct integration for Composer and PSR standards.

Onboard to an Unfamiliar PHP Codebase with Atlas (2026)

Build a mental model of an unfamiliar PHP repo in 2026 without reading every file. Atlas maps the Composer layout, delegates sweeps to a read-only explore subagent.

Debug a Single Failing Test in PHP with Atlas (2026)

Debug one failing PHPUnit test with Atlas in 2026: isolate it with the filter flag, walk the call path with the lsp tool, and fix the PHP code, not the assertion.

Add a regression test for a bug fix in PHP with Atlas (2026)

Add a PHP regression test for a bug fix in 2026: Atlas proves the PHPUnit test fails red first, applies the fix with edit, and re-runs the same command to prove green.

Self-review your working diff before committing in PHP with Atlas in 2026

Catch your own mistakes in PHP code before they reach a reviewer or CI. Atlas helps PHP developers in 2026 self-review uncommitted diffs, run PHPUnit tests, and apply PHP-CS-Fixer formatting.

Extract a shared helper from duplicated code in PHP with Atlas (2026)

The same PHP logic is copy-pasted in 5 controllers with different variable names. Atlas finds it by meaning with codebase_search, extracts one helper, and proves it with PHPUnit.

Diagnose a Hanging or Long-Running PHP Command With Atlas (2026)

Is your Composer install slow or blocked on stdin? Atlas's bash timeout message tells you which, and PHP-specific non-interactive flags get the command unstuck.

Browse this resource hub