PHP Application Audit: The Methodology Behind a Comprehensive Code Review

#php application audit
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

A PHP application audit gets commissioned for one of three reasons. Someone is about to buy the company that owns the code. Someone inherited a system nobody on the current team wrote. Or someone is standing at a fork between refactoring what exists and rewriting it, and does not want to make that call on instinct.

All three need the same thing: an objective read on the state of a codebase that the people closest to it can no longer see clearly. What they usually get instead is a static analysis report with four thousand warnings, which answers nothing. This post describes how the review is actually structured, what each pass looks at, and what has to end up in the report for it to be worth the money.

What a comprehensive PHP application audit covers

Security audits are a narrower exercise and deserve their own treatment. A full application audit is broader and examines seven dimensions, because a codebase can be secure and still be unmaintainable, or clean and still be architecturally stuck.

Architecture coherence. Not whether the architecture is fashionable, but whether it is consistent. Most aging PHP applications contain two or three architectural eras layered on top of each other: a procedural core, a half-finished MVC migration, and a newer Symfony or Laravel section. That layering is normal. What matters is whether the boundaries between eras are explicit or whether business logic leaks across all three, which is what makes every change expensive.

Security surface. Input handling, authentication and session management, authorization checks at the right layer, secrets handling, file upload paths, SQL construction, and deserialization. The audit maps where untrusted data enters and follows it through the system.

Data model health. Schema normalization, index coverage against the actual query patterns, foreign key integrity, nullable columns that quietly encode meaning, and the number of places that write to the same table. A data model with fifteen writers and no invariant enforcement is a slow-motion consistency incident.

Test coverage and quality. Coverage percentage is close to meaningless on its own. What matters is which paths are covered. An application at 70 percent coverage that omits payment, authentication, and permission checks is less safe than one at 35 percent that covers exactly those.

Performance hotspots. Query counts per request, N+1 patterns, missing caching layers, synchronous work that belongs in a queue, and memory behaviour under realistic load rather than on a developer laptop.

Dependency hygiene. Abandoned packages, versions with known advisories, the distance between the installed PHP version and a supported one, and how much of the dependency tree is transitively pinned by a single outdated package.

Deployment pipeline. Whether a release is reproducible, whether rollback is a real option, how migrations run, and how long it takes to get a one-line fix into production. A team that cannot deploy safely will not refactor safely either.

How the review is structured

The order matters. Running tooling before you understand the domain produces a pile of findings with no priority attached to any of them.

Phase 1: Discovery interview

Two to three hours with whoever knows the system best, plus whoever owns the budget. The questions worth asking are rarely technical:

  • Which parts of the application does the team avoid touching, and what happens when they have to?
  • What was the last incident, and what was the actual root cause?
  • Which features are on the roadmap for the next two quarters?
  • What is the revenue path through this system, and which code sits on it?
  • Who wrote the oldest parts, and are they still reachable?

The roadmap question is the one that changes the audit most. A module that is ugly but frozen is not a priority. The same module sitting under three planned features is the most expensive thing in the codebase.

Phase 2: Automated analysis

Tooling is the cheap pass, so it runs early and broadly. On a PHP codebase that means PHPStan or Psalm at increasing levels to map the type safety baseline, PHP_CodeSniffer for standards drift, composer audit for known advisories, PHPMD or a complexity metric for hotspot detection, and a coverage run if a test suite exists.

The output is not the finding. It is the map. Static analysis tells you where to look, and the density of warnings per directory is usually a better signal than any individual warning. A directory with ten times the error density of its neighbours is where the manual pass starts.

One practical note: on an unaudited codebase, PHPStan level 0 alone will often produce thousands of errors. Running level by level and recording the count at each level gives a far more useful shape than a single number. It shows whether the code is uniformly weak or whether a handful of files carry the debt.

Phase 3: Manual review of critical paths

This is where the time goes and where the value is. Automated tooling cannot tell you that the discount calculation is subtly wrong, that authorization is checked in the controller but not in the queue consumer that hits the same service, or that two modules maintain conflicting definitions of what an active customer is.

The paths reviewed by hand are selected from the discovery interview: the revenue path, the authentication and authorization path, anything handling personal data under GDPR, the highest-traffic read path, and whichever module the roadmap targets next. Everything else gets sampled rather than read line by line.

Phase 4: Report and walkthrough

The written report is the deliverable, but the walkthrough is what makes it land. An hour with the team going through the top findings in the actual code surfaces context that changes severity in both directions, and it converts the report from a verdict into a plan.

What the report contains

A report that lists problems without ranking them shifts the hardest decision back to the client. Three components make it actionable.

Severity-ranked findings. Each finding gets a severity, a location, a description of the concrete failure mode, and an estimate of remediation effort. Severity is a function of likelihood and blast radius, not of how much the reviewer dislikes the code. A missing authorization check on an admin endpoint is critical. A god class of 2,000 lines that has not changed in three years is a note, not an emergency.

Remediation recommendations. Specific, sequenced, and honest about cost. "Introduce a service layer" is not a recommendation. "Extract order state transitions into a single OrderWorkflow service, migrate the four call sites listed below, and add characterization tests in the same pull request, roughly five to eight days" is one.

A risk register. The findings that are not going to be fixed soon still need to be visible: what could go wrong, what the early warning signal looks like, and what the contingency is. This is the section that gets read by the non-engineers, and it is often the reason the remediation budget gets approved.

What the findings tend to look like

From an anonymized engagement on a nine-year-old Symfony application, roughly 180,000 lines, six developers, running in production for a mid-size logistics client. The top findings:

  1. Authorization enforced in controllers only. The same domain services were reachable from a Messenger consumer and two CLI commands with no permission checks. Not exploited, but one queue message away from being.
  2. Twenty-three writers to the shipment status column, with no state machine and four different definitions of a valid transition. The root cause behind three of the last five production incidents.
  3. A read endpoint issuing 340 queries per request, because a Twig template lazily traversed a collection inside a loop. It had been "the slow page" for two years and nobody had profiled it.
  4. PHP 8.1 on a codebase that could run on 8.3, held back by one abandoned PDF library used in exactly two places.
  5. Coverage at 61 percent, with the billing module at 4 percent.

None of these required an exotic technique to find. They required someone reading the code with no prior assumption that the existing structure made sense. The billing coverage gap in particular was known internally and had been quietly reclassified as acceptable, which is what familiarity does to risk perception over time.

Timelines, and what moves them

A focused audit of a small application, under 50,000 lines with a single framework and a running test suite, is typically three to five days. A mid-size application in the 100,000 to 250,000 line range with mixed architectural eras runs two to three weeks. Larger or genuinely undocumented systems go beyond that.

Four factors move the number more than raw size does:

  • Whether the application runs locally. If a reviewer cannot boot the system in the first day, the audit becomes archaeology. This is the single most common cause of overrun.
  • Architectural uniformity. One consistent framework is far faster to assess than three eras of half-migration, regardless of line count.
  • Test suite presence. Tests are documentation of intent. Without them, every behavioural question becomes a manual trace.
  • Domain complexity. Insurance, logistics, and billing domains carry rules that are not inferable from the code, so more interview time is needed.

Ask for the boot instructions before the engagement starts. It is the cheapest schedule protection available.

Turning the findings into a backlog

The report is an input, not a plan. Converting it usually follows a simple sequence.

Fix the critical security and data integrity findings first, regardless of effort. These are the ones where the cost of waiting is unbounded.

Next, take the intersection of the finding list and the next two quarters of roadmap. Structural work in code you are about to touch anyway pays for itself immediately. Structural work in code nobody will open pays for itself never.

Then set a ratchet rather than a target. Commit a PHPStan baseline, make CI fail if it grows, and let the debt shrink as people touch files. This is more durable than a cleanup sprint, because it survives the first urgent interruption.

Leave the rest in the risk register with a review date. A finding that stays untouched for a year and causes no trouble was correctly deprioritized, and that is useful information too.

Refactor or rewrite

The question that prompts most audits deserves a direct answer, and the audit is what makes the answer defensible rather than emotional.

Refactoring wins when the data model is broadly sound, the domain logic is correct even if badly organized, and the team can still ship. Under those conditions the code is an asset in poor packaging, and the years of accumulated edge case handling are worth more than they look.

A rewrite becomes reasonable when the data model itself encodes the wrong domain concepts, when the platform is genuinely at end of life with no migration path, or when nobody left can describe what the system does. Those are narrower conditions than most teams assume in the middle of a frustrating quarter. In practice the honest answer is often "refactor the core, rewrite one bounded module, and stop discussing the rest", and having the finding list in front of you is what makes that conversation concrete. A legacy code optimization programme that starts from an audit tends to be scoped realistically, because the unknowns have already been priced.

Getting a second opinion

An audit is worth commissioning when a decision depends on it and the internal read is contested. If everyone already agrees on what is wrong and what to do about it, spend the money on the fix instead.

At Wolf-Tech we run this process on PHP and Symfony codebases across Europe, usually before a modernization programme, a funding round, or an acquisition. If you want to talk through whether an audit is the right next step for your system, code quality consulting is where that starts. Write to hello@wolf-tech.io or have a look at wolf-tech.io, and we will tell you honestly whether an audit is what you need.