PHP · Code rescue

Auditing a legacy PHP codebase before you inherit it

"Can you take this over?" usually arrives with a zip file, a couple of nervous emails, and an outgoing developer who wants to hand off this week. Whether the codebase is worth taking on — and what it will cost — is decided in the next 90 minutes of reading. Here is the audit I run before I quote, and the report I deliver.

Auditing a legacy PHP codebase

The two shapes of audit

It helps to know what kind of audit you are doing because the audience and the deliverable differ:

  • Pre-acquisition audit. A buyer is paying you to tell them whether to acquire the codebase. The deliverable is a written report addressed to the buyer with a recommend / don't-recommend, the risks they would inherit, and a remediation cost estimate.
  • Takeover audit. The owner has decided to switch developers and wants to know what they're handing you. The deliverable is the same shape but the audience is the owner, and you're often quoting on the takeover yourself, so the report doubles as your scope-of-work justification.

The technical work is similar. The framing is different. Be clear up front which one you are running — it changes which findings you emphasise.

The first 90 minutes

Before you read a single line of business logic, read these:

  1. composer.json and composer.lock. Tells you the framework version, the PHP version requirement and every third-party dependency. Anything pinned to an unmaintained or wildly outdated version is a future migration cost. The lock file tells you the actual versions in production.
  2. README.md. Read it asking "if I had to deploy this tomorrow, could I?" If the README is missing or only describes "this is the company X website", that absence is the finding. Onboarding cost just went up by a week.
  3. .env.example or equivalent. Lists every external dependency the application needs to run — databases, queues, S3 buckets, payment gateways, mail providers, third-party APIs. This is where you discover the system has eight integrations nobody mentioned.
  4. Deploy configuration. A Dockerfile, a CI workflow, a deploy script, anything that captures how this thing reaches production. Absence is again the finding — every deploy currently lives in someone's head.
  5. Top-level folder structure. Spend two minutes scanning. A clean structure tells you the previous developer cared about organisation. A folder named old/, backup/ or v2/ at the root is its own story.

Do not run the code yet. Do not open the IDE. Just read the meta. By the end of these five steps you usually know whether this is a 4-week takeover or a 4-month rescue.

Complexity heuristics

Now sample the code itself. You don't need to read everything — you need a representative slice. I look at:

  • The five largest files. A 4,000-line controller is a problem you can quote on. Ten of them is a structural problem you have to plan around.
  • Whatever the framework calls a "controller" or "route" — the request entry points. If most controller actions are 200+ lines with database queries, validation, business logic and rendering all mixed in, the cost of changing anything is high.
  • The three deepest folders. Deep nesting is a sign that someone tried to impose order; whether it succeeded depends on whether the names mean anything.
  • Duplicated code. Search for a couple of obvious phrases — a query that loads the current user, a function that formats currency, a permission check. If the same logic exists in three places with small variations, every change in business rules requires three diffs and you'll miss one.

Tools that help if you want a numeric handle: phploc for size and complexity stats, a duplicate-code detector, and a dependency analyser. Numbers are nice but the human reading is more important — the metric tells you "this might be a problem", the reading tells you whether it actually is.

The security smell pass

Spend 20 minutes grepping for the patterns that explain most PHP CVEs. You're not doing a deep security audit here — you're checking whether the previous developer followed the basics.

# Direct superglobal use without sanitization
grep -rn '\$_GET\[\|\$_POST\[\|\$_REQUEST\[' --include='*.php' src/ app/ | head -50

# Raw SQL concatenation
grep -rn 'wpdb->query\|DB::raw\|mysqli_query\|mysql_query' --include='*.php' . | head -30

# Inline echo without escaping (sample)
grep -rn 'echo \$' --include='*.php' . | head -30

# Suspicious eval / dynamic include patterns
grep -rn 'eval(\|include \$\|require \$' --include='*.php' . | head -20

You're not chasing a clean grep — you're sampling. Five hits where a query string flows straight into SQL means there are probably fifty. The audit's job is to flag the pattern, not to enumerate every instance.

The database story

Three questions, one report paragraph each:

  1. Are schema changes versioned? Look for a migrations/ directory, a schema.sql, or in WordPress a custom-table version flag. If there is nothing — and the answer to "how do you change the schema" is "we run ALTER manually on production" — that is a critical finding.
  2. Is there an ORM, or raw queries everywhere? Mixed is fine. Raw everywhere is a maintenance burden but not a fatal flaw — many systems run that way. ORM with occasional raw queries for performance is healthy.
  3. Are foreign keys real, or implicit? An "implicit" foreign key — a column called customer_id with no constraint — means you cannot trust the data. Orphans accumulate. Reports lie. The fix is a migration plan.

If you can, get a database dump (anonymised) and look at the actual data. The schema tells you what the developer intended. The data tells you what actually happened.

What "tests" actually means here

"Yes, we have tests" can mean anything from "30,000 lines of green PHPUnit" to "one file from a tutorial nobody has run since". The honest answer comes from running them.

  • Do they pass on a fresh checkout?
  • How long does the suite take? A 45-minute test run gets skipped before deploys, which means the tests aren't actually protecting anything.
  • What is the coverage of the parts that handle money, permissions or destructive operations? That's where it matters; you can ignore coverage of the simple CRUD pages.
  • Are there any integration tests that exercise the database, or only unit tests of pure functions?

A codebase with no tests at all is not necessarily a no-go. A codebase with a broken test suite that everyone has been ignoring for a year is worse — it's actively misleading.

Production access posture

This is the most-skipped step and the most expensive one to get wrong. Before you take over, you need to know:

  • Where is production hosted, and who pays the bill?
  • Who currently has SSH or panel access? Are credentials shared in a password vault or in a Slack DM from 2019?
  • Is there a staging environment that mirrors production, or are deploys to production the only deploys?
  • Where are backups, who runs them, when were they last tested by restoring?
  • Where do logs go, and how long are they retained?
  • What monitoring exists? "We notice when customers call" is a finding.

Half of these are not a code problem — they are an operations problem. But they are your problem the moment you take over. If they look bad, scope a 1-week ops handover into the rescue project: rotated credentials, vaulted secrets, documented deploy, basic monitoring. Doing it on day one prevents you carrying someone else's outage.

Quick wins vs structural debt

By the end of the audit you will have a long list of issues. Sort them into two buckets:

  • Quick wins. One-day fixes that materially reduce risk. Rotating leaked credentials, adding a basic backup, putting defined('ABSPATH') at the top of every file, deleting the old/ directory full of half-finished code, putting the database behind a private network. These earn early trust with the new owner and demonstrate value.
  • Structural debt. Multi-week refactors that change how the code is organised. Extracting business logic from controllers, introducing migrations, replacing a homegrown ORM with the framework's. These are projects, not tickets.

The mistake is to start the rescue with the structural work. You haven't earned permission yet. Bank a week of quick wins, then propose the structural plan with a working relationship behind you.

Writing the report

The report is short. It has six sections:

  1. Executive summary. Three to five sentences. Recommendation. Headline risks. Indicative cost.
  2. Architecture & structure. What the codebase is, what shape it is in, what the major risks are.
  3. Security findings. Critical, high, medium. Sampled, not exhaustive. State that explicitly.
  4. Operations & ops debt. The ops list above with severity per item.
  5. Prioritised remediation plan. Quick wins first (week 1), structural work after (month 1+).
  6. Cost & engagement model. What it would take to do the remediation. Fixed-fee where possible, T&M where the unknowns are too large.

Write it for the decision-maker, not for an engineer. They need to know "is this acquisition a yes" or "is this rescue a six-week or six-month project". Save the engineering detail for an appendix.

Pricing the rescue

Here is the unglamorous truth: rescues are usually under-priced. The previous developer's mess is your problem to discover, and the discovery happens after you start. Two pricing patterns work:

  • Phased fixed-fee. Phase 1 is a 1- to 2-week stabilisation with a fixed price (the quick-wins list). Phase 2 is the structural work, scoped after Phase 1 because by then you actually know the codebase. The buyer gets price certainty on the part that matters most; you get permission to scope the bigger work from a position of knowledge.
  • Capped time-and-materials. Hourly rate with a not-to-exceed cap and a defined deliverable. Best for cases where the scope is genuinely unknowable up front but you've shown enough trust through the audit to be hired at all.

What does not work: a fixed price for the whole rescue quoted from the audit alone. You will eat the difference and resent the project.

Inheriting a codebase and want a written audit before you commit? Send me what you have — I deliver fixed-fee audits with a written report inside one week.