Start Here

Managing Multiple PHP Versions

Real PHP developers often work on more than one project. One project may require PHP 8.2, another may require PHP 8.3, and a new project may already use PHP 8.5 features.

Managing multiple versions is about control. You need to know which version each project expects, which version your shell is running, which version the web server is running, and which extensions are available for that version.

The Project Decides The Version

The first place to check is usually composer.json.

{
  "require": {
    "php": "^8.3",
    "ext-mbstring": "*",
    "ext-pdo": "*"
  }
}

Then verify the active runtime:

php -v
composer check-platform-reqs

If Composer says your PHP version is not supported, do not change application code. Change or select the correct runtime.

Find Every Visible PHP Binary

On macOS or Linux:

which php
which -a php
php -v

On Windows PowerShell:

where php
php -v

The first path is the one your terminal uses. Other paths may belong to Homebrew, XAMPP, Laragon, MAMP, system packages, custom builds, or version managers.

Switching Versions

How you switch depends on your setup:

  • Homebrew can link a different formula.
  • Linux packages may provide versioned binaries such as php8.2 and php8.3.
  • Windows may switch by changing PATH order.
  • Tools such as Herd, Valet, asdf, phpenv, or containers can manage versions per project.
  • Docker pins the version in the image tag, such as php:8.3-fpm.

The exact tool matters less than the verification:

php -v
php --ini
php -m
composer check-platform-reqs

Run those after switching.

CLI And Web Versions Can Differ

One common mistake is switching CLI PHP but leaving the web runtime unchanged.

Example:

CLI: PHP 8.3
FPM: PHP 8.2

That can produce bugs where Composer works but the website fails, or the browser cannot use a syntax feature that works in terminal scripts.

Check CLI:

php -v
php --ini

Check FPM or the web runtime through service status, pool configuration, or a small web-only diagnostic route:

PHP example
<?php

declare(strict_types=1);

header('Content-Type: text/plain');

echo 'PHP: ' . PHP_VERSION . PHP_EOL;
echo 'SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Loaded ini: ' . (php_ini_loaded_file() ?: 'none') . PHP_EOL;

Do not leave this public in production. Use it temporarily in a safe environment or behind access control.

Extensions Are Version-Specific

Installing an extension for PHP 8.2 does not automatically make it available for PHP 8.3.

Check extensions after every version switch:

php -m
php -m | grep mbstring
php -m | grep pdo

From PHP:

PHP example
<?php

declare(strict_types=1);

foreach (['mbstring', 'intl', 'pdo'] as $extension) {
    echo $extension . ': ';
    echo extension_loaded($extension) ? 'loaded' : 'missing';
    echo PHP_EOL;
}

// Prints:
// mbstring: loaded
// intl: loaded
// pdo: loaded

If a project fails after switching PHP versions, missing extensions are one of the first things to check.

Containers Make Versions Explicit

Containers are often the cleanest way to pin project runtime versions.

FROM php:8.3-fpm

That image tag makes the expected version visible. You still need to install required extensions in the image and run checks inside the container:

php -v
php -m
composer check-platform-reqs

Do not rely on host PHP when the project actually runs in a container.

Upgrade Checks

When moving a project from one PHP version to another, verify more than syntax.

Useful checks:

php -v
composer check-platform-reqs
composer update --dry-run
php -l public/index.php

Also run the project's test suite and static analysis if available.

Look for:

  • removed or deprecated features
  • changed extension availability
  • dependencies that do not support the new PHP version
  • FPM service still running the old version
  • CI using a different version from local development

Read Version Constraints As Support Contracts

A Composer constraint is not merely a suggestion for one developer's machine. It describes PHP versions the package or application claims to support.

For example:

{
  "require": {
    "php": "^8.2"
  }
}

Within PHP 8, ^8.2 permits PHP 8.2 and later PHP 8 releases before 9.0. It also means source code must remain parseable and functional on PHP 8.2 unless the project deliberately raises its minimum.

Check composer.lock and project documentation too. The lock file records resolved package versions but does not replace the root PHP constraint. A dependency set resolved on PHP 8.5 can still be unsuitable for a promised PHP 8.2 deployment if platform handling was misconfigured.

Use Composer to investigate blockers before switching a whole project:

composer prohibits php 8.4

This reports packages or root constraints preventing the proposed platform version. It does not prove application behaviour on that runtime.

Understand Composer Platform Emulation

A root project can configure a platform PHP version for dependency resolution:

{
  "config": {
    "platform": {
      "php": "8.2.0"
    }
  }
}

This makes Composer resolve dependencies as though PHP 8.2.0 were present. It does not change the interpreter running Composer, tests, or application code. A developer on PHP 8.5 can still accidentally write PHP 8.5 syntax unless CI and analysis check the actual minimum.

Composer's check-platform-reqs command checks the real PHP process and extensions and ignores config.platform. Use both concepts deliberately: emulation can constrain dependency resolution, while real-runtime checks prove the current environment.

Do not add or change config.platform.php merely to silence a dependency error. It can hide the fact that the real deployment uses another version. Record why the project needs it and test the declared minimum independently.

Select A Version Per Project

Global switching is simple but affects every terminal and project using that command. Per-project tools reduce accidental cross-project changes.

A controlled project can use:

  • a version-manager file recognised by the team's tool;
  • a development app's per-site version selection;
  • a container image tag and wrapper command;
  • a Nix, dev-container, or similar environment definition;
  • explicit versioned binaries in scripts where appropriate.

Commit only configuration supported by the team. A .tool-versions file does nothing for contributors who do not use a compatible manager, and a Dockerfile does not select host PHP for a manually run Composer command.

Document one authoritative command for Composer, tests, static analysis, and framework tooling. Avoid a workflow where composer install runs on host PHP 8.5, tests run in container PHP 8.2, and the editor analyses as PHP 8.4 without anybody knowing.

Switch The Complete Toolchain

After selecting a PHP branch, verify more than php -v:

php -v
php --ini
php -m
composer --version
composer check-platform-reqs

Also verify:

  • Composer is launched by the intended PHP process;
  • PHPUnit or Pest supports that PHP branch;
  • static-analysis target versions match the project;
  • coding-standard and refactoring tools run on compatible versions;
  • debugger extensions are installed for the selected branch;
  • web/FPM and queue workers have switched where required.

A test runner can require a newer PHP version than the application supports. Projects may solve this with compatible tool versions, containers, or separate analysis environments, but the support matrix must be explicit.

Switch Web Services Without Leaving Mixed Traffic

Changing a CLI symlink does not change PHP-FPM pools, Apache modules, application servers, or running workers. A rolling deployment can temporarily contain multiple runtime versions.

Before using syntax available only in the new version:

  1. make the current code compatible with both old and new runtimes;
  2. add the new runtime to CI and resolve deprecations and dependency issues;
  3. deploy the compatible code;
  4. upgrade every web node, worker, scheduler, and utility process;
  5. verify the fleet no longer executes the old version;
  6. raise the declared minimum and tool language levels;
  7. only then merge syntax requiring the new baseline.

This avoids an old worker parsing files containing unsupported syntax. Version upgrades are fleet changes, not only package-manager commands.

Keep Extension Sets Per Version

Each PHP branch has its own module API and often its own INI tree. Maintain a project extension inventory rather than assuming the previous branch's modules followed the switch.

Check concrete drivers and extension configuration:

php -m
php --ri intl
php -r 'print_r(PDO::getAvailableDrivers());'

When switching back to an older project, repeat the checks. A global extension configuration that works on the newest branch may name directives or modules unavailable on the older one.

Debugger and profiler extensions can also affect performance and startup. Enable them intentionally per environment rather than copying one complete INI directory between versions.

Test The Minimum And The Target

For a library supporting several PHP branches, CI should test a meaningful version matrix, especially the minimum and current versions. For an application, CI should include the production baseline and proposed target during migration.

The minimum runtime catches unsupported syntax and dependency drift. The newest supported runtime exposes deprecations and future compatibility work. Static analysis can supplement this but does not replace execution on relevant runtimes.

Use syntax linting with the target interpreter. PHP 8.5 cannot prove that a file parses on PHP 8.2; it only proves that PHP 8.5 accepts it.

Roll Back A Version Switch Cleanly

Before switching a shared environment, record:

  • currently selected executable and service;
  • package or image versions;
  • configuration and extension inventory;
  • database or cache compatibility concerns;
  • deployment artifact and rollback command;
  • verification metrics and logs.

Rollback should restore the complete runtime and service configuration, not just the php symlink. If application code or dependencies have already adopted new-version requirements, rolling back only the interpreter can make the application unparsable.

Common Multiple-Version Failures

Symptom Likely cause
php -v changed but website did not FPM, Apache, or application server still uses the old runtime
Composer resolves newer packages unexpectedly Real runtime or config.platform differs from the intended minimum
Tests fail after switching back Test runner or dependencies no longer support the older PHP branch
Extension missing only on one branch Modules and INI trees are version-specific
Editor accepts syntax CI rejects IDE language level differs from the CI minimum
One terminal reports another version Shell startup, shim, container, or command cache differs
Rollback fails immediately Code or lock file now requires the newer runtime

The goal is not to make every tool report the newest PHP. It is to make each project and execution context report the version that its documented support contract requires.

Version Notes Belong In The Project

A project should record:

  • required PHP version
  • local version switching tool
  • container image tag, if any
  • required extensions
  • how to verify CLI PHP
  • how to verify web PHP
  • how to run Composer platform checks

This avoids tribal knowledge and makes onboarding faster.

What You Should Be Able To Do

After this lesson, you should be able to identify a project's required PHP version, find all visible PHP binaries, switch or select the correct runtime, verify extensions, distinguish CLI from web PHP, and explain why containers can make version management clearer.

For junior PHP work, this matters because version mismatches waste time and cause misleading bugs. Good developers prove the runtime before blaming the application.

Practice

Practice: Design A Two-Project PHP Version Workflow
  • Project A: "php": "^8.2", production PHP 8.2, with config.platform.php set to 8.2.0;
  • Project B: "php": "^8.5", containerized on a PHP 8.5 FPM image.

The host currently runs PHP 8.5, while an old PHP 8.2 FPM service still serves Project A.

Requirements

Create a workflow that:

  • identifies the support contract and actual runtime for each project;
  • explains what Composer platform emulation does and does not do;
  • defines authoritative commands for Composer, tests, CLI, and web execution;
  • verifies extensions and PDO drivers after every switch;
  • checks the IDE/static-analysis language level;
  • prevents PHP 8.5 syntax entering Project A;
  • includes a temporary, restricted web-runtime diagnostic and its removal;
  • defines how Project A could later migrate to PHP 8.5 without mixed-runtime parse failures;
  • records rollback evidence.
Show solution

Project A promises PHP 8.2 support, so its source must parse and work on PHP 8.2. config.platform.php constrains Composer resolution as though PHP 8.2.0 were present; it does not change the host interpreter or prevent PHP 8.5-only syntax.

Run Project A's Composer, lint, tests, and analysis under an actual PHP 8.2 environment selected by the team's version manager or container. Verify:

php -v
php --ini
php -m
php -r 'print_r(PDO::getAvailableDrivers());'
composer check-platform-reqs

Configure the IDE and static analyser for PHP 8.2 and keep PHP 8.2 in CI. Confirm the existing FPM pool and CLI use the intended branch with a temporary restricted diagnostic showing version, SAPI, INI path, and required-extension presence; remove it immediately.

Project B's authoritative commands run inside its PHP 8.5 container. Host PHP is not evidence for the FPM image. Run Composer, tests, platform checks, and runtime inspection through the repository's documented container service.

To migrate Project A, first make code and dependencies compatible with both versions, add PHP 8.5 CI, and deploy compatible code. Upgrade every web node, FPM pool, queue worker, scheduler, and utility process. Verify no PHP 8.2 process remains, then raise Composer and analysis baselines before allowing PHP 8.5 syntax.

Record the old package/image version, FPM unit and pool, extension set, INI paths, deployment artifact, and rollback commands. A rollback must restore compatible code and dependencies as well as the interpreter.