Version Compatibility in Practice: Manual, Composer, and Frameworks

Determine whether a given language feature, extension, Composer package, or framework version is compatible with a target PHP version, using the PHP manual, Composer platform constraints, and framework migration guides, and plan an upgrade window around published deprecation timelines.

Every PHP developer eventually needs to answer some version of the same question: 'will this work on the PHP version I'm targeting?' — whether that's a language feature you want to use, an extension a project depends on, a Composer package under consideration, or an entire framework's next major release. This lesson is about developing a reliable, repeatable process for answering that question correctly, rather than guessing, assuming the newest thing is always safe, or finding out the hard way in production. It builds directly on the previous two lessons: recognizing legacy patterns and planning a migration both depend on being able to check compatibility facts precisely, and that's the specific, practical skill this lesson covers.

Checking Language Feature Availability in the PHP Manual

The single most authoritative source for whether a given PHP language feature or built-in function exists in a specific PHP version is the official PHP manual at php.net. Every function reference page includes a 'Changelog' table near the bottom listing the PHP version each significant change was introduced in — a new optional parameter, a changed default, a deprecation, or the version the function itself first appeared. For language features (like readonly properties, enums, or first-class callable syntax) rather than individual functions, the manual's page for that feature states plainly, usually right in the introductory paragraph, something like 'As of PHP 8.1.0, ...' — that phrase is the single fact you're looking for.

A disciplined habit worth building: before using any language feature or function you're not 100% certain has existed since your project's minimum supported PHP version, open its manual page and check the version note before writing the code, rather than after a bug report comes in from someone running an older PHP than you assumed. This is especially important when a team's local development environment runs a newer PHP version than production — it's entirely possible to write and test code locally that works perfectly, and then have it fail with a syntax or fatal error the moment it's deployed, purely because the feature you used exists in your local PHP but not in production's.

Beyond the manual itself, the RFC (Request for Comments) process that governs PHP's own evolution is a useful secondary resource for understanding *why* a feature was added or changed a certain way and exactly which version it landed in — every accepted PHP RFC on the wiki records its target version, and reading the RFC for a feature you're unsure about (readonly properties, enums, named arguments) often answers edge-case questions the manual's prose glosses over.

• The PHP manual's per-function 'Changelog' table is the authoritative source for when a function or its behavior was introduced or changed. • Language-feature manual pages state their introduction version directly, typically as 'As of PHP X.Y.0, ...'. • Check version notes before writing code with an unfamiliar or recent-feeling feature, not after a deployment failure. • A newer local PHP version than production is a classic, easy-to-miss source of 'works on my machine' feature-availability bugs. • PHP RFCs on the wiki record a feature's target version and rationale, useful for edge cases the manual doesn't fully cover.

Extension Compatibility: Built for a Specific PHP Version and Architecture

PHP extensions — compiled, often C-based modules like pdo_mysql, gd, imagick, redis, or xdebug — are not portable the way pure-PHP code is. An extension binary is compiled against a specific PHP version's internal ABI (Application Binary Interface), and typically against a specific PHP build's thread-safety mode (NTS, non-thread-safe, vs ZTS, Zend Thread Safe) and CPU architecture (x86_64 vs ARM64) as well. An extension .so file (on Linux) or .dll file (on Windows) built for PHP 8.1 will not load on PHP 8.2 — attempting to enable it produces an error at PHP startup along the lines of 'PHP Startup: Unable to load dynamic library ... wrong ELF class' or a version-mismatch message, because the internal structures the extension expects don't match what the newer PHP version actually provides.

This means that upgrading PHP's version on a server is never just a matter of installing the new PHP binary — every compiled extension the application needs must also have a build available for that exact new version. On most Linux distributions, this is handled transparently by the package manager: installing php8.2-redis alongside php8.2 pulls in an extension build already compiled for that specific PHP version, and package repositories that offer multiple PHP versions side by side (like the Ondřej Surý PPA on Ubuntu) publish a matching set of extension packages per version. When building PHP or its extensions manually — or using PECL directly (pecl install redis) — the extension is compiled from source against whatever PHP version's development headers are currently active, so the PHP version must be switched *before* running the PECL install, not after.

In practice, checking extension compatibility means confirming, before an upgrade, that every extension your application currently loads (visible via php -m) has an available, maintained build for the target PHP version — most well-maintained extensions publish this clearly in their own release notes or package repository, but a rarely-updated or community-maintained extension is exactly the kind of thing that can silently block an otherwise-ready PHP upgrade, so it's worth checking early rather than assuming.

• Extensions are compiled binaries tied to a specific PHP version's ABI, thread-safety mode (NTS/ZTS), and CPU architecture. • An extension built for one PHP version will not load on a different one — it fails at PHP startup with a load/version-mismatch error. • Distro package managers usually publish matching per-version extension packages (e.g. php8.2-redis for php8.2). • PECL compiles an extension from source against whichever PHP version's dev headers are currently active at install time. • Check that every currently-loaded extension (php -m) has an available build for the target PHP version before upgrading.

Composer's Platform Requirements: the php Constraint

Composer, PHP's dependency manager, lets a project declare exactly which PHP version(s) it supports through a "php" entry inside the "require" block of composer.json — for example "php": "^8.2" means 'PHP 8.2.0 or any later 8.x version, but not 9.0'. This constraint is enforced during composer install and composer update: if the PHP version actually running Composer doesn't satisfy it, Composer refuses to install and reports exactly which requirement failed, preventing an application from being deployed onto (or a developer from working against) a PHP version its own dependencies were never verified to support.

Every third-party package a project depends on carries its own "php" platform constraint too, and Composer's dependency resolver checks all of them together — if package A requires "php": ">=8.1" and package B requires "php": "^7.4", those two packages cannot be installed together under any PHP version at all, since there's no PHP version satisfying both, and Composer will report this as a resolution conflict rather than silently picking one. This is precisely why the previous lesson's advice to check dependency PHP-version support *before* upgrading matters in concrete, mechanical terms: Composer itself will refuse to proceed if the numbers don't line up.

Sometimes a CI environment or a Docker image running Composer doesn't have PHP itself installed at all (only Composer, which is technically possible since Composer is itself just a PHP script but can run against a minimal interpreter, or platform checks are being run against a different environment than where the code will ultimately execute), or a team deliberately wants to develop against a newer local PHP than a slightly older production target. Composer supports a config.platform override for exactly this situation: setting "config": {"platform": {"php": "8.1.29"}} in composer.json tells Composer to *pretend* the running PHP version is 8.1.29 for the purpose of dependency resolution, regardless of what PHP version is actually executing Composer — useful for keeping a composer.lock file resolved against production's real PHP version even when a developer's or CI runner's local PHP differs.

• "require": {"php": "^8.2"} in composer.json declares which PHP versions the project supports. • Composer enforces this constraint on every install/update, refusing to proceed if the running PHP version doesn't satisfy it. • Every dependency's own php constraint is checked together — conflicting ranges across packages cause a hard resolution failure. • config.platform.php overrides Composer's view of the running PHP version, useful when local/CI PHP differs from production's. • The platform override affects dependency resolution only — it does not change which PHP version actually executes the code.

Framework Version Requirements

Major PHP frameworks pin each of their own major (and often minor) versions to a specific supported PHP version range, published prominently in their own documentation and enforced through their own composer.json platform constraints — meaning a framework upgrade and a PHP upgrade are frequently coupled decisions, not independent ones. Laravel, for instance, has historically required a new minimum PHP version with nearly every major release: Laravel 9 required PHP 8.0+, Laravel 10 required PHP 8.1+, and Laravel 11 required PHP 8.2+ — so a team wanting to stay on the latest Laravel release is implicitly committing to keeping PHP itself reasonably current too, and conversely, a team stuck on an old PHP version for other reasons will eventually be unable to upgrade Laravel further until PHP is addressed first.

Symfony follows a similar pattern with its own version-to-PHP-version mapping, published in its own documentation and release notes, and both frameworks publish detailed upgrade guides for every major version bump listing every breaking change, deprecated API, and required code change — the same kind of migration guide discussed in the previous lesson, just scoped to the framework rather than to PHP itself. Reading a framework's own upgrade guide before starting a version bump is exactly as important as reading PHP's own migration guide, and for the same reason: it's the authoritative, version-specific source, more reliable than guessing from a changelog summary or a blog post that may already be out of date.

This coupling is a good reason to check framework PHP requirements *before* deciding on a PHP upgrade target, not after: if the goal is 'upgrade to the latest Laravel', the required PHP version is a hard floor dictated by the framework, not a free choice; if the goal is 'upgrade PHP as far as we reasonably can', the currently-installed framework version's own PHP ceiling may need raising first, which brings its own separate migration guide and effort into the plan.

• Each major framework release pins a specific minimum (and sometimes maximum) supported PHP version, enforced via its own composer.json. • Example: Laravel 9 requires PHP 8.0+, Laravel 10 requires PHP 8.1+, Laravel 11 requires PHP 8.2+. • A framework upgrade and a PHP upgrade are often coupled decisions — one can force the other. • Always read the framework's own official upgrade guide for the specific version jump being made, not just PHP's migration guide. • Check framework PHP requirements before settling on a PHP upgrade target, since the framework may set a hard floor or ceiling.

Reading Deprecation Timelines and Planning an Upgrade Window

PHP's own manual, and virtually every major framework and package, communicate upcoming breaking changes through a consistent pattern of language: 'deprecated in version X' means the feature still works in version X (usually accompanied by a runtime deprecation notice, visible if error_reporting includes E_DEPRECATED) but is scheduled for removal in a specific future version; 'removed in version Y' means the feature no longer exists at all as of that version and code using it will fail outright, typically with a fatal error for a function call or a parse error for removed syntax. Reading a deprecation notice carefully — noting both numbers, X and Y — turns a vague sense of 'this old thing should probably get fixed eventually' into a concrete deadline: everything deprecated in PHP 8.1 and removed in PHP 9.0 must be fixed in the codebase before the project can safely upgrade to PHP 9.0, but there is no similar hard deadline forcing a fix before then.

This timeline-reading skill is what lets a team plan an upgrade window deliberately rather than reactively. A practical approach: when a new PHP version is released, read its 'What's new' and 'Backward incompatible changes' pages immediately (they're published alongside the release, often months in advance for the beta/RC period), search the codebase for anything the release deprecates using the compatibility and static-analysis tooling from the previous lesson, and schedule the fixes for those deprecations to land well before the *next* major version (the one that actually removes the feature) is even released — treating a deprecation notice as an early warning with a generous grace period, not something to ignore until it becomes a fire.

The same reading skill applies to framework and package deprecation notices, which typically follow an identical 'deprecated in X.Y, to be removed in (X+1).0' pattern in their own changelogs — the practice of checking 'what's the deprecation version, what's the removal version, and how much runway does that actually give us' generalizes across the entire PHP ecosystem, and is arguably the single most transferable skill from this whole module: it turns unpredictable-feeling breaking changes into a schedule you control.

• 'Deprecated in X' means still works in X (often with an E_DEPRECATED notice); 'removed in Y' means it's gone entirely as of Y. • Read both version numbers together to get a concrete deadline, not just a vague sense that something should be fixed eventually. • Read a new PHP version's 'Backward incompatible changes' page as soon as it's published, ideally during its beta/RC period. • Schedule fixes for newly-deprecated features to land well before the version that actually removes them ships. • The same 'deprecated in X, removed in Y' reading pattern applies to framework and package changelogs, not just PHP core.

Code Examples

"php": "^8.2" declares the project needs PHP 8.2.0 or later 8.x releases, matching what "laravel/framework": "^11.0" itself requires (Laravel 11 needs PHP 8.2+) -- these two constraints are consistent with each other, which is exactly what to check before locking them in. The ext-pdo and ext-mbstring entries declare required extensions explicitly, so Composer fails fast with a clear message if either is missing, rather than the application failing later with a confusing runtime error. config.platform.php pins dependency resolution to a specific patch version (8.2.18), useful for keeping composer.lock reproducible in CI even if a developer's local PHP differs slightly.

Both enum and readonly were introduced in PHP 8.1 per their manual pages -- writing this code in a project whose composer.json says "php": "^8.0" creates a real inconsistency: the code will work fine in a PHP 8.1+ development environment but will fail to even parse on the PHP 8.0 the project claims to support, which is exactly the kind of mismatch a compatibility check should catch before deployment.

The deprecation note alone doesn't force action if the codebase doesn't yet require the version that removed it, but recording it as a tracked, scheduled fix -- searched for and counted now, while there's no time pressure -- turns a future breaking change into planned work instead of a surprise discovered mid-upgrade.

{
    "name": "sampleapp/course-app",
    "require": {
        "php": "^8.2",
        "laravel/framework": "^11.0",
        "ext-pdo": "*",
        "ext-mbstring": "*"
    },
    "require-dev": {
        "phpstan/phpstan": "^1.11",
        "phpunit/phpunit": "^10.5"
    },
    "config": {
        "platform": {
            "php": "8.2.18"
        },
        "sort-packages": true
    }
}
<?php
declare(strict_types=1);

// Before writing this, check the PHP manual: enums were added
// "As of PHP 8.1.0" -- so this code has a hard floor of PHP 8.1.
enum OrderStatus: string {
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

// readonly properties: also "As of PHP 8.1.0" per the manual.
final class Order {
    public function __construct(
        public readonly int $id,
        public readonly OrderStatus $status,
    ) {}
}

// If composer.json declares "php": "^8.0", this file would be
// using features the project's own declared minimum doesn't support --
// a mismatch worth catching with static analysis before it ships.
# Example reasoning process for a real deprecation, written out:
#
# PHP manual note on a hypothetical function:
#   "Deprecated as of PHP 8.3.0. This function will be removed
#    in a future version of PHP without a direct replacement."
#
# 1. Search the codebase for every call site:
grep -rn "risky_legacy_function(" src/

# 2. Confirm the current composer.json floor:
#    "php": "^8.1"   <-- project already supports 8.3, no rush yet,
#                         but the function is already deprecated there.
#
# 3. Decide the deadline: fix every call site before the project
#    ever raises its "php" constraint to the version that removes
#    the function -- track it as a ticket now, not an emergency later.
echo "Tracked: replace risky_legacy_function() before PHP 9.0 support is required"

CI Pipelines and the platform Override

Teams running Composer inside a minimal CI container that doesn't exactly match production's PHP patch version commonly rely on config.platform.php in composer.json to keep dependency resolution consistent with what production actually runs, ensuring the committed composer.lock file reflects production's real constraints rather than whatever PHP happens to be installed in that particular CI image.

// Assuming a language feature works everywhere because it works on the developer's local PHP install

// Check the PHP manual's version note ("As of PHP X.Y.0") against the project's actual composer.json "php" constraint before using it

A local development environment often runs a newer PHP version than production or than the project's declared minimum. Code that works locally can fail to even parse on the PHP version the project actually claims to support, so the manual's version note has to be checked against the project's real floor, not against what happens to be installed on one machine.

sudo cp redis.so /usr/lib/php/8.2/extensions/ # copied from a PHP 8.1 install sudo pecl install redis # compiled fresh against the active PHP 8.2 installation's headers

An extension binary is compiled against a specific PHP version's internal ABI. Copying a .so file built for PHP 8.1 into a PHP 8.2 extensions directory produces a load failure at startup rather than a working extension -- extensions must be built (or obtained pre-built) for the exact target PHP version.

{"require": {"php": "*", "laravel/framework": "^11.0"}} {"require": {"php": "^8.2", "laravel/framework": "^11.0"}}

Leaving the php constraint as a wildcard doesn't actually make the project more compatible -- it just hides the real requirement that laravel/framework ^11.0 already forces (PHP 8.2+) behind an uninformative wildcard, so Composer can't warn early when someone tries to run the project on an incompatible PHP version.

Where should you check to confirm exactly which PHP version introduced a specific language feature? A. Guess based on how long ago you first heard about the feature B. The official PHP manual page for that feature, which states its introduction version directly (e.g. 'As of PHP 8.1.0') C. Any blog post that mentions the feature D. The feature will work the same on every PHP version, so no check is needed

B. The official PHP manual page for that feature, which states its introduction version directly (e.g. 'As of PHP 8.1.0') — The PHP manual is the authoritative source and states a feature's introduction version explicitly in its documentation, along with a changelog table for individual functions -- this is far more reliable than guessing or relying on secondary sources that may be outdated.

Why can't a compiled PHP extension built for PHP 8.1 simply be copied and used on a PHP 8.2 installation? A. Extensions are actually plain PHP files and this would work fine B. The extension binary is compiled against a specific PHP version's internal ABI, thread-safety mode, and architecture, which differ between versions C. PHP extensions expire automatically after one year regardless of version D. Extensions only work with even-numbered PHP minor versions

B. The extension binary is compiled against a specific PHP version's internal ABI, thread-safety mode, and architecture, which differ between versions — Extensions are compiled binaries tied to a specific PHP version's internal structures (ABI), thread-safety build, and CPU architecture. A mismatch causes a load failure at PHP startup, so an extension must be built (or obtained pre-built) specifically for the target PHP version.

What does Composer's config.platform.php setting actually do? A. It installs that exact PHP version on the machine automatically B. It overrides Composer's view of the running PHP version for dependency resolution purposes only, without changing which PHP version actually executes code C. It deletes any dependency that doesn't support that PHP version D. It has no effect and is purely documentation

B. It overrides Composer's view of the running PHP version for dependency resolution purposes only, without changing which PHP version actually executes code — config.platform.php tells Composer to pretend the given PHP version is running, for the purposes of resolving which package versions are compatible -- useful when local or CI PHP differs from production -- but it does not itself install or switch the actual PHP interpreter.

If a function is documented as 'deprecated in PHP 8.1' and 'removed in PHP 9.0', what is the correct way to read that? A. The function already stopped working as of PHP 8.1 B. The function still works through the 8.x line (usually with a deprecation notice) but will be gone entirely starting in PHP 9.0, giving a concrete deadline to fix it before then C. The two version numbers are unrelated and can be ignored D. It means the function is now faster starting in PHP 8.1

B. The function still works through the 8.x line (usually with a deprecation notice) but will be gone entirely starting in PHP 9.0, giving a concrete deadline to fix it before then — "Deprecated in X" means the feature still functions in version X (typically with a runtime notice) but is scheduled for removal; "removed in Y" is the actual hard deadline after which code using it fails outright. Reading both numbers together turns a vague future risk into a concrete, plannable deadline.

Why is a framework's PHP version requirement (e.g. Laravel 11 requiring PHP 8.2+) relevant when planning a PHP upgrade? A. It isn't relevant -- frameworks and PHP versions are always independent B. Because a framework upgrade and a PHP upgrade are often coupled: the framework's own required PHP version range can set a hard floor or ceiling on which PHP version is achievable C. Because frameworks automatically upgrade PHP for you during installation D. Because PHP itself requires a specific framework to run at all

B. Because a framework upgrade and a PHP upgrade are often coupled: the framework's own required PHP version range can set a hard floor or ceiling on which PHP version is achievable — Each major framework release pins its own required PHP version range in its documentation and composer.json. Wanting the latest framework version can force a PHP upgrade, and being stuck on an old PHP version can block a framework upgrade until PHP is addressed -- making the two decisions coupled rather than independent.

A project's composer.json currently declares "php": "^8.0", but a developer wants to start using readonly properties (added in PHP 8.1) and the team also wants to upgrade to Laravel 11 (which requires PHP 8.2+). Write the corrected "require" section of composer.json that makes all three facts consistent, and explain in one sentence why the old "^8.0" constraint was a problem even before Laravel 11 was considered.

{"require": {"php": "^8.2", "laravel/framework": "^11.0"}} -- the old "^8.0" constraint was already inconsistent with the plan to use readonly properties, since those require PHP 8.1+, meaning the codebase would have used a feature its own declared minimum PHP version didn't support even before Laravel 11 (which independently raises the floor to 8.2) entered the picture.

The php constraint needs to be at least as high as the strictest requirement among all the features and dependencies actually being used.