Migrating legacy PHP code to modern Composer packages
Legacy PHP applications rarely arrive as one clean codebase. They often combine procedural controllers, PEAR libraries, PECL extensions, include_path lookups and framework helpers that assume the host has been configured in a particular way. A Composer-based package migration can remove that guesswork, but only if the old coupling is understood before it is hidden behind autoloading.
The practical goal is not a wholesale rewrite. It is a reproducible build: one manifest, one lock file, an explicit extension list, and deployment steps another engineer can repeat from a clean machine. That gives small Australian software teams a safer way to retire brittle deployment scripts without changing business behaviour in the same release.
Why Composer changes the risk profile
Composer turns loosely coupled library references into a declared dependency graph. Namespaces and PSR-4 autoloading replace fragile path matching, while composer.lock records the exact versions used for a release. The application and its operating system become easier to describe, rebuild and audit as a single unit.
The difficult work sits at the boundaries. A legacy global function may belong in an autoloaded files entry, a class may need a namespace before it can be discovered, or a bundled library may conflict with the version available through Packagist. The external technical material can provide a second reference while the team compares those choices, but the application’s own tests must remain the authority on behaviour.
Composer also changes how releases are operated. A production deployment should install from the committed lock file rather than resolve fresh versions during the release. This creates a repeatable package baseline and makes rollback a matter of restoring a known artefact, not hoping a remote registry still serves the same code.
Mapping dependencies before changing code
Start with an inventory rather than a composer require command. Search for require, include, get_include_path, dynamic class names, registered shutdown functions and extension checks. Record PEAR and PECL dependencies separately because some PECL modules still need to be installed by the operating system or container image rather than by Composer.
Next, translate the dependency map into composer.json. Keep production requirements in require, development tools in require-dev, and use replace, provide or conflict only when the relationships are genuine. Packages that were once copied into the repository should be classified first: a clean upstream release may become a normal requirement, while an internally modified fork may need a secured VCS or artefact repository. Composer plugins and package scripts can execute code during installation, so review them with an explicit allow-plugins policy and keep registry credentials in CI secrets.
Before refactoring names or includes, capture the current behaviour. Exercise login, checkout, scheduled jobs, email delivery and administrative workflows with automated tests where possible. Add PHPStan or Psalm as a baseline rather than treating every existing warning as a migration failure. A measured baseline lets the team distinguish pre-existing defects from regressions introduced by the package conversion.
A practical migration sequence
The first wave should be small enough to deploy and reverse within one business day. Pick one module with clear ownership and limited use of global state, then move through the following sequence.
- Record the current PHP version, enabled extensions, environment variables, cron jobs, writable paths and public URLs.
- Create or correct the relevant entries in
composer.json, keeping development-only tools out of the production dependency set. - Convert directory classes to namespaced PSR-4 paths and load procedural function files through an explicit
autoload.filesentry. - Deploy to a staging or canary environment, compare outputs and remove the old include or PEAR installation only after the new path is proven.
Once the module works, make the checks part of continuous integration. The same commands should run locally and in the build pipeline, with failures stopping publication of an artefact.
- Run
composer validate --strictto catch malformed metadata and abandoned package constraints. - Run
composer install --no-dev --prefer-dist --no-interactionto build from the committed lock file. - Run
composer check-platform-reqsandcomposer audit, then execute unit and integration tests plus the PHPStan suite. - Build an immutable deployment artefact, store its checksum, and promote that exact artefact through staging and production.
Treat exceptions as recorded decisions. A private repository, an unusual extension or a package that cannot meet the supported PHP baseline may require a wrapper service, a container change or a temporary fork. Every exception needs an owner, a reason and an expiry date instead of remaining hidden in an old server configuration.
Aligning packages with production infrastructure
Application dependencies are only half of the runtime. PHP extensions, ICU data, OpenSSL, timezone files and the PHP-FPM or CLI configuration must match the versions used during testing. Container images can make this repeatable by installing extensions in the image, locking the base image and running Composer in a controlled build stage. The production host should receive the built artefact, not build unreviewed packages while traffic is flowing.
For containerised workloads, my notes on container monitoring with cAdvisor show how runtime measurements can be collected alongside deployment metrics. Container CPU and memory figures are useful, but PHP request latency, worker saturation, database time and queue depth still need application-level visibility. A migration is complete only when the team can see whether the new package layout changed performance.
Australian infrastructure choices add constraints that are easy to miss in generic advice. Many local hosting providers still offer cPanel and Plesk environments, where the available PHP version and extensions may differ from those in a current container image. Sydney and Melbourne cloud regions can keep latency low for local users, yet backups, support access and disaster recovery may cross borders. If the system handles personal information, document its Australian Privacy Act 1988 and Australian Privacy Principles obligations, including breach-response responsibilities. Also check that daylight-saving changes in eastern states cannot cause cron-based batch jobs or expiry logic to run twice or miss a scheduled window.
A controlled rollout and next step
Release the migrated code behind a feature flag or route a small share of traffic to it while the old implementation remains available. Keep the previous lock file, container image and configuration snapshot together so rollback is a tested operation rather than a hopeful reconstruction. After observation, remove duplicated code and old infrastructure deliberately; carrying both paths indefinitely makes the next upgrade harder.
The same separation of trust and traffic applies to supporting networks. My explanation of VLANs and managed switches covers a practical approach to segmenting management, storage and user devices, whether the project is a home lab or a small business environment. Composer does not secure an exposed PHP application by itself, so firewalling, patching and least-privilege access belong in the same migration plan.
If you want outside help with this work, I provide independent infrastructure planning, cloud migration and systems architecture advice. Contact me through this site’s contact details to arrange a scoped review of the legacy code, Composer manifest, CI pipeline and target hosting environment. The engagement can deliver a staged package baseline, a verified rollback path and deployment documentation your team can operate with confidence.
Karl Katzke