Engineering

What “We Will Just Modernise the PHP App” Really Means

A PHP application is not modernised by changing its framework or adding a new frontend. The real work is discovering hidden contracts, controlling risk and replacing behaviour without interrupting the business.

6 min read
A wireframe drawing of a building half weathered solid mass and half fresh scaffold framework, with poles crossing the seam.

“Just modernise it” can mean a runtime upgrade, framework migration, interface redesign, cloud move, domain rewrite or all of them at once. A mature PHP application often contains years of business decisions in controllers, database triggers, scheduled scripts, templates and manual operating procedures. Some behaviour is wrong, some looks odd but protects an important exception, and some is no longer used. Replacing code without distinguishing those categories turns unknown behaviour into production incidents. Modernisation should begin as risk reduction: make the system observable and testable, understand its contracts, then change it in increments whose business effect can be compared and reversed.

Discover the system that actually runs

Inventory runtime and extension versions, web-server configuration, package and copied-library dependencies, entry points, cron jobs, queues, file exchanges, database procedures, writable directories and external services. Trace authentication, session handling and administrative routes. Compare source control with deployed artefacts; manual production edits and environment-specific files are common risks. Use access logs and database observations carefully to identify live routes and workload patterns, but do not assume absence in a short sample means dead code. Interview operators about month-end jobs, recovery steps and exports consumed elsewhere. The deliverable is a map of execution and ownership, including processes that sit outside the main web application.

Stabilise before restructuring

Bring the current application into a repeatable build and deployment process. Externalise secrets, pin dependencies, establish supported backups and test restoration. Add request correlation, central error reporting, health checks and basic service metrics without logging sensitive payloads. Run static analysis and dependency vulnerability checks, then triage findings according to exploitability and change risk. A container can make runtime dependencies reproducible, but it does not modernise unsafe code or poor operations by itself. Stabilisation provides the feedback needed for later changes. If a deployment cannot be reproduced or rolled back, even a small refactor carries an unnecessarily large operational consequence.

Characterise behaviour before improving it

Legacy systems often lack tests, and adding unit tests to tightly coupled code may require the refactor they are meant to protect. Begin with characterisation tests at stable boundaries: HTTP requests and responses, generated files, important database effects and scheduled-job outcomes. Capture representative fixtures with sensitive data removed. These tests document current behaviour, including quirks, without declaring all of it desirable. Add focused tests around calculations, permissions and transitions as seams emerge. For critical workflows, compare old and new outputs in shadow mode or replay controlled inputs. The goal is enough evidence to tell an intentional change from an accidental one, not an arbitrary coverage percentage.

  • Deployed runtime, extensions, dependencies and unsupported components.
  • Routes, jobs, scripts, integrations and reports that carry business obligations.
  • Database ownership, triggers, implicit constraints and shared-table consumers.
  • Security boundaries, session behaviour and privileged operations.
  • Recovery procedures, cut-off windows and people who understand exceptions.

Separate runtime, framework and product change

Combining a major PHP upgrade, framework rewrite, schema redesign and visual rebuild makes failures difficult to locate and rollback difficult to execute. Sequence changes where possible. Compatibility tooling and deprecation logs can support runtime upgrades; adapters can isolate old framework services; a new interface can consume a stable application boundary. Sometimes an old framework cannot run on a supported runtime, forcing a coupled step, but that should be recognised explicitly. Maintain a decision record describing why each stage exists and what proves it safe. Users should not have to relearn a workflow merely because the deployment platform changed unless product change is an intentional, tested part of the scope.

Treat the database as a shared contract

Direct SQL scattered through pages can conceal transaction boundaries and schema assumptions. First identify which applications, reports and scripts read or write each critical table. Add constraints only after profiling existing violations and planning remediation. Use expand-and-contract migrations: introduce compatible fields or tables, deploy code that can tolerate both states, backfill with reconciliation, switch reads and finally remove the old shape. Large alterations need testing against production-like volume and locking behaviour. Monetary values require decimal semantics, and dates need an explicit time-zone policy. Do not let old and new code update the same concept independently without a single ownership rule and conflict strategy.

Replace along controlled seams

The strangler pattern is useful when traffic or capabilities can be routed independently. Put an explicit gateway or route boundary in front, move one coherent capability and keep identity and observability consistent across both sides. Avoid creating a distributed monolith where every new request synchronously calls deep legacy internals. An anti-corruption layer can translate legacy data and terminology while ownership moves. For lower-risk internal modules, an in-place modular refactor may be simpler than services. Choose boundaries from business cohesion and transaction needs, not a desire to advertise microservices. Every extracted capability needs deployment, monitoring and support, so extraction should buy meaningful independence.

Security work cannot wait for the new system

Known risks in the existing application remain live throughout a multi-stage programme. Patch supported dependencies where possible, remove unused administrative routes, rotate embedded credentials and place compensating controls around code that cannot yet change safely. Review SQL construction, output encoding, file uploads, deserialisation, cross-site request protections and session cookie settings. Use parameterised queries and context-appropriate escaping rather than broad input filtering. Privileged functions need server-side authorisation on every action; hiding a link in a redesigned interface is not access control. Rate limits and an application firewall may reduce exposure, but neither proves unsafe application behaviour is corrected. Modernisation also changes threats. Running old and new stacks can duplicate identity configuration, secrets and public endpoints. A gateway must not accept one stack’s session without a deliberate trust protocol, and temporary data synchronisation must preserve tenant and permission boundaries. Security tests should accompany every routed capability, including attempts to access another user’s records and replay state-changing requests. Maintain an inventory of temporary exceptions with expiry and owner. Decommissioning matters: once a route moves, remove its legacy exposure and credentials rather than leaving a hidden alternative path. Incremental delivery is safer only when the transition architecture receives the same security scrutiny as the intended destination.

Security work cannot wait for the replacement. Patch supported dependencies, rotate embedded credentials, remove unused administration routes and place explicit controls around code that cannot yet change. Review parameterised SQL, context-specific output encoding, uploads, deserialisation, cross-site request protection and session settings. Privileged actions need server-side authorisation regardless of what the new interface hides. Running old and new stacks can duplicate identity, secrets and endpoints, so threat-model the transition architecture itself. When a route moves, remove its legacy exposure and credentials rather than leaving a hidden alternative. Incremental delivery reduces risk only when temporary trust relationships have owners, tests and expiry conditions.

Define modernisation as measurable control

Set outcomes such as running on a supported runtime, reproducible deployment, known dependencies, tested critical journeys, reduced privileged surface and independently changeable modules. Establish current evidence and verify each increment against it. Use feature flags and reversible routing, reconcile dual-run outputs where appropriate and retain a tested rollback. Retire old code, jobs and credentials once traffic and obligations have moved; a permanent parallel estate is not completion. The practical plan is discovery, stabilisation, characterisation, carefully separated upgrades and incremental replacement. That may sound less simple than “just modernise”, but it turns a vague rewrite into a sequence of controlled engineering decisions.

Apply the thinking to your system.

Share the architecture, constraints and decision you are facing. We will respond to the engineering problem in front of you.