PHP 8.2 End of Life Is December 31, 2026: What Laravel Teams Should Do Now

PHP 8.2 end of life lands December 31, 2026. Here is the compliance risk, a dated migration plan for Laravel teams, and the rollback path if it goes wrong.

Richard Joseph Porter
20 min read
phplaravelupgradesdevopssecuritylegacy-modernization

On December 31, 2026, PHP 8.2 stops receiving security fixes. Not bug fixes — those ended two years ago, on December 31, 2024. Security fixes. After that date, a critical vulnerability in the PHP interpreter running your Laravel application will be published, exploited, and never patched upstream.

As I write this, that is roughly sixteen weeks away. That is enough time to do the work properly, and not much more than enough. This post is not a tour of PHP language features — that lives in PHP 8.3 to 8.5 for Laravel teams. This one is about the deadline: what actually expires, what it does to your compliance posture, and a dated plan that gets a production Laravel application off 8.2 before the calendar runs out.

What Actually Expires on December 31, 2026

PHP branches get two years of active support and two more years of security-only support. Here is the current state from the official supported versions page, which you should check yourself rather than trusting this table six months from now.

Version Released Active support until Security support until
8.2 Dec 8, 2022 Dec 31, 2024 Dec 31, 2026
8.3 Nov 23, 2023 Dec 31, 2025 Dec 31, 2027
8.4 Nov 21, 2024 Dec 31, 2026 Dec 31, 2028
8.5 Nov 20, 2025 Dec 31, 2027 Dec 31, 2029

Two dates share that December 31, 2026 line, and confusing them is the single most common planning error I see:

  • PHP 8.2 leaves security support. It becomes end of life. No further releases of any kind.
  • PHP 8.4 leaves active support. It keeps security fixes for two more years, until the end of 2028.

So 8.4 is a legitimate destination — it just is not a long one, and it stops receiving ordinary bug fixes on the same day 8.2 dies. PHP 8.5 is the only version that still has active support after this year. If nothing in your dependency graph forces you to stop at 8.4, aim at 8.5 and do the work once.

One more thing worth being precise about, because it changes the urgency for some teams. If you run PHP from a distribution package rather than upstream — RHEL's application streams, Ubuntu Pro's expanded security maintenance, or a vendor like Sury — your distributor may backport security fixes past upstream EOL under their own support contract. That is a real mitigation, not a fiction. But it is a contractual mitigation with its own end date, it does not cover PECL extensions, and it does not make php -v reporting 8.2 any easier to defend in a vendor security questionnaire. Check what you are actually entitled to before you rely on it.

The Compliance Clock Runs Faster Than the Technical One

Running an EOL interpreter is not just a hypothetical CVE problem. It fails audits, and it fails them on a schedule you do not control.

PCI DSS. If you handle cardholder data, the standard requires that system components be protected from known vulnerabilities by installing applicable vendor-supplied security patches — Requirement 6.3.3 in the v4.x text — alongside the secure software development requirements in Requirement 6.2. An interpreter for which no vendor patches exist cannot satisfy "install applicable security patches." Confirm the exact clause numbering against the current version of the standard with your QSA; the requirement itself has been stable across revisions even when the numbering has not.

SOC 2. There is no line item that says "do not run PHP 8.2." What there is, under the common criteria for change management and risk mitigation, is an expectation that you identify and remediate known vulnerabilities on a defined timeline. An auditor who finds an EOL runtime in scope will write it up, and your remediation plan becomes an audit exception you carry into the next cycle.

Vendor and enterprise security reviews. This is the one that costs revenue first. Enterprise procurement questionnaires routinely ask whether all software components are within their supported lifecycle. Answering "no" on a renewal or a new deal does not always kill it, but it reliably adds weeks and a remediation commitment with a date attached.

Your own dependency graph. Package maintainers drop EOL PHP versions from their CI matrices quickly. Within a quarter or two of 8.2's death, you will start seeing packages whose new majors require ^8.3 or ^8.4, which means you stop receiving their security patches too. The interpreter is the first domino, not the only one.

The practical framing I use with clients: after December 31, 2026, every week you stay on 8.2 you are accumulating an unpatched-vulnerability exposure that compounds, with no upper bound and no scheduled relief.

Where Laravel Fits: Upgrade PHP First, Then Laravel

Here is the framework side, from the Laravel support policy:

Laravel Supported PHP Released Bug fixes until Security fixes until
10 8.1 – 8.3 Feb 14, 2023 Aug 6, 2024 Feb 4, 2025 (EOL)
11 8.2 – 8.4 Mar 12, 2024 Sep 3, 2025 Mar 12, 2026 (EOL)
12 8.2 – 8.5 Feb 24, 2025 Aug 13, 2026 Feb 24, 2027
13 8.3 – 8.5 Mar 17, 2026 Q3 2027 Mar 17, 2028

If you are on PHP 8.2, you are almost certainly on Laravel 11 or 12. Laravel 11 went fully end of life on March 12, 2026 — which means a team on Laravel 11 and PHP 8.2 is running two unsupported components and needs to treat this as a security remediation, not a roadmap item. Laravel 12 has security fixes until February 24, 2027, so you have a genuine planning window, but only just.

Do the PHP upgrade first, and ship it separately. The reason is mechanical: Composer resolves php as a platform requirement. Laravel 13 requires ^8.3, so on 8.2 the framework update will not even resolve — you cannot half-ship it. But there is a better reason than mechanics. Bundling both into one branch gives you a single enormous diff where every red test is ambiguous: framework behavior change, or PHP deprecation? Separating them gives you attributable failures, independent rollback, and a review a human will actually read.

The ordering rule, which is the same one in my legacy Laravel migration playbook: move PHP as far as your current Laravel supports, ship it, soak it, then move the framework. On Laravel 12 that means you can go all the way to 8.5 without touching the framework, and the Laravel 13 hop becomes a separate, small project you can schedule for Q1 2027. That sequencing is exactly why upgrading to Laravel 13 reads as a short job for teams who did the runtime work first, and a nightmare for teams who did not.

The Dated Plan: September to December 2026

Sixteen weeks, working backwards from a hard December 31 date and reserving the last two weeks as buffer, because nobody wants to be doing a runtime cutover during a change freeze.

Window Milestone Exit criteria
Sep 8 – Sep 19 Baseline and scoping composer why-not php 8.4 output triaged; deprecation logging live in production
Sep 22 – Oct 10 Package remediation Every blocking dependency has a decision: upgrade, fork, replace, or inline
Oct 13 – Oct 24 PHP 8.3 branch green in CI Suite passes with --fail-on-deprecation
Oct 27 – Nov 14 PHP 8.4 branch green in CI Rector run reviewed and merged; PHPStan baseline clean
Nov 17 – Nov 28 Staging soak + canary 24h+ on staging with the full runtime; one canary worker pool in production
Dec 1 – Dec 12 Production ramp 10% → 50% → 100% of web traffic, error rate and p95 flat
Dec 15 – Dec 19 8.5 hop (optional) Only if 8.4 is stable everywhere
Dec 22 – Dec 31 Buffer / freeze No deploys; you are done

If you are on Laravel 11, insert the framework chain after this, in Q1 2027. If you are on Laravel 10 or older, that timeline does not fit and you should be scoping honestly against what a Laravel upgrade actually costs rather than pretending sixteen weeks is enough.

Week 1: Establish a Baseline You Can Trust

This is the half-day that produces your real estimate. Do not skip it and do not guess.

# What are you actually running? Not what the README says.
php -v
php -m                                # loaded extensions
php --ini                             # which ini files are in play

# What blocks the target? This is the most predictive command here.
composer why-not php 8.3
composer why-not php 8.4
composer why-not php 8.5

# Can your current lockfile even install on the target?
composer check-platform-reqs

# Dependency health
composer outdated --direct
composer audit

composer why-not php 8.4 names every package whose constraint caps below your target. That list — not your application code, not the framework — is what decides whether this is a two-week job or a two-month one. When it names something, find out who pulled it in before you decide what to do about it:

composer why doctrine/dbal
composer depends --tree spatie/laravel-permission

A blocking package that is a direct dependency is a decision. A blocking package four levels deep behind something you barely use is often a deletion.

Then turn on deprecation logging on your current 8.2 production runtime and leave it for a week. Static analysis finds what it can see; this finds what your users actually hit.

// config/logging.php
'deprecations' => [
    'channel' => 'deprecations',
    'trace' => true,
],

'channels' => [
    'deprecations' => [
        'driver' => 'daily',
        'path' => storage_path('logs/deprecations.log'),
        'level' => 'debug',
        'days' => 30,
    ],
],

Make sure the runtime is not silently swallowing them. A surprising number of production php.ini files inherited an error_reporting value from 2016 that masks exactly the notices you now need:

; php.ini - during the migration window
error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
zend.exception_ignore_args = Off   ; keep args in traces while you migrate
# Confirm what the running process sees, not what the file says
php -r 'printf("error_reporting=%d\n", error_reporting());'
php -i | grep -E "error_reporting|display_errors|error_log"

Weeks 2–3: Pin Composer's Platform and Clear the Blockers

The config.platform.php key is the gotcha nobody mentions. It tells Composer which PHP version to resolve against, regardless of what is installed. Leave it pinned at 8.2 while your servers move to 8.4 and Composer will happily keep resolving old package versions while you wonder why nothing updates.

{
    "require": {
        "php": "^8.3"
    },
    "config": {
        "platform": {
            "php": "8.3.0"
        }
    }
}

Bump it in lockstep with your real floor at each stage, or remove it entirely and let Composer read the runtime. To test against a version a dependency has not declared support for yet, ignore only the upper bound:

composer update --ignore-platform-req=php+

The + suffix means "ignore the upper limit only." Useful for an advisory CI job. Never for production.

What Actually Breaks Between 8.2 and 8.4

The full language-level walkthrough is in the 8.3 to 8.5 deep dive. Here is the short list of what generates real diffs in a Laravel codebase, so you can size the work.

Implicit nullable parameters (deprecated in 8.4). This is the dominant diff. Foo $bar = null must become ?Foo $bar = null. Every custom middleware signature, every query scope with an optional filter. In a five-year-old codebase, expect hundreds of occurrences. Do not fix them by hand:

<?php
// rector.php

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/app',
        __DIR__ . '/config',
        __DIR__ . '/database',
        __DIR__ . '/routes',
        __DIR__ . '/tests',
    ])
    ->withPhpSets(php83: true, php84: true)
    ->withImportNames(removeUnusedImports: true);
vendor/bin/rector process --dry-run    # read the diff before you trust it
vendor/bin/rector process
vendor/bin/pint                        # normalise the formatting churn

Run Pint immediately after Rector and commit them as two separate commits. Rector's output is functionally correct and stylistically inconsistent; mixing the formatting churn into the semantic diff makes the PR unreviewable. If you want a second pass over that diff before it reaches a human reviewer, the workflow in my post on AI-assisted code review for Laravel is exactly what I use for mechanical refactors like this one.

Dynamic property deprecation fallout (8.2). Writing to an undeclared property was deprecated in PHP 8.2 and is scheduled for removal in PHP 9. It is not removed yet — but if you have been on 8.2 for two years with deprecations suppressed, you have been accumulating these silently. They surface as log noise the moment you turn on the deprecation channel above. Declare the properties, or annotate the class with #[\AllowDynamicProperties] as a documented, temporary exception. Eloquent models are exempt from this in normal use because attribute access goes through __get, but your service classes, DTOs, and anything that inherited from a 2018 base class are not.

#[\Override] (added in 8.3). Not a breaking change, but the most useful thing you can adopt during this project. Marking a method as an override makes PHP verify at compile time that a parent method actually exists with that name — which catches the exact class of bug a framework upgrade introduces when a base class method gets renamed.

final class SendInvoiceNotification extends Notification
{
    #[\Override]
    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }
}

E_STRICT (8.4). The error level was removed and the constant deprecated. If your php.ini, Docker entrypoint, or a stray error_reporting() call still contains E_ALL & ~E_STRICT, that is a deprecation notice on every request. This one almost always lives in infrastructure, not application code:

rg -n "E_STRICT" . --glob '!vendor' --glob '!node_modules'

exit() and die() became functions (8.4). They now behave like function calls rather than language constructs, which means they respect declare(strict_types=1) and perform type coercion instead of silently casting to string. exit(0) and bare exit; are fine. exit($someValueFromConfig) under strict types in a console command or a deploy script is the shape that now throws a TypeError. Grep your artisan commands and any standalone CLI scripts.

mysqli and session deprecations (8.4). Several MYSQLI_* constants were removed outright — MYSQLI_SET_CHARSET_DIR, MYSQLI_STMT_ATTR_PREFETCH_ROWS, and others — and functions including mysqli_ping(), mysqli_kill(), and mysqli_refresh() were deprecated. PHP 8.4 also tightened validation on session INI settings such as session.gc_divisor and session.gc_probability. Check the official 8.4 migration guide for the complete list rather than trusting any blog summary, including this one.

rg -n "mysqli_|new mysqli|MYSQLI_" app/ scripts/ database/ --glob '!vendor'

In a Laravel context the fix is almost never "update the mysqli call" — it is "move this to the query builder or PDO." A connection-liveness check written with mysqli_ping() in 2016 marks a code path that never got the Eloquent treatment, and those legacy raw-SQL corners are usually where the worst query shapes hide too. Worth reading them against Laravel database performance: N+1 and indexing while you are in there.

Extensions and PECL: The Blocker Nobody Budgets For

Application code is the visible half. Extensions are where deploys actually fail, because they compile per PHP version and their release cadence lags core by weeks or months.

The ones that reliably matter for Laravel stacks:

  • ext-redis — the cache, session, queue, and Horizon backend for most Laravel apps. Verify the version in your image, not on your laptop, and re-check behavior if you use the native session handler rather than Laravel's own driver.
  • ext-imagick — historically among the slowest to publish builds after a PHP minor release, and it depends on the system ImageMagick version too. If you generate thumbnails or PDFs, snapshot-test the output across the version bump. Image pipeline differences pass CI and produce a mangled customer-facing invoice.
  • ext-xdebug — CI and local only, but it will stop your pipeline dead if the matrix installs a version with no build for your target. Pin it explicitly.
  • ext-swoole / ext-openswoole / FrankenPHP — if you run Octane, the application server is a hard dependency on the PHP version and the single most likely thing to hold you at 8.4. Check its supported-version matrix before you commit to a target, and re-run your load profile after the bump: long-lived workers surface state leaks that a per-request app never shows.
  • APM agents — New Relic, Datadog, Blackfire, Elastic. These fail quietly. A missing APM extension will not break your test suite. It will break your observability, in production, exactly when you need it during a canary.
# Pin the exact patch tag. "8.4-fpm-alpine" is a moving target and you
# will not enjoy discovering that mid-incident. Set this to the current
# patch release at build time and bump it deliberately.
ARG PHP_IMAGE_TAG=8.4.x-fpm-alpine
FROM php:${PHP_IMAGE_TAG}

COPY --from=mlocati/php-extension-installer:latest \
     /usr/bin/install-php-extensions /usr/local/bin/

RUN install-php-extensions \
      pdo_mysql redis intl bcmath gd zip opcache pcntl

# Verify at build time, not at 3am
RUN php -m | sort && php -v

Two runtime settings to reconsider rather than carry forward. Opcache compiles opcode that is version-specific: if you use opcache.file_cache, point the new version at a fresh directory, because a stale cross-version cache produces failures that look like nothing else you have ever seen. And pm.max_children should be re-measured, because per-worker memory footprint shifts between versions — do not reuse a number someone calculated in 2022. If your PHP-FPM sizing is also your cloud bill, that memory measurement pairs directly with the AWS cost optimization audit checklist.

CI Matrix: Test the Version You Are Leaving and the One You Are Going To

# .github/workflows/tests.yml
name: tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.2', '8.3', '8.4']
        include:
          - php: '8.5'
            experimental: true

    continue-on-error: ${{ matrix.experimental == true }}

    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, bcmath, pdo_mysql, redis, gd, zip
          ini-values: error_reporting=E_ALL, display_errors=On, memory_limit=512M
          coverage: none

      - run: composer update --prefer-dist --no-interaction --no-progress

      - run: vendor/bin/phpstan analyse --no-progress

      - name: Tests (deprecations are failures)
        run: vendor/bin/pest --fail-on-deprecation --fail-on-warning

Keep 8.2 in the matrix until the cutover is complete — that is your rollback target, and a green build on it is what makes rollback a redeploy instead of a re-resolve under pressure. Pin PHPStan to a range so it reports anything that breaks on any version in your mixed fleet:

# phpstan.neon
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    level: 6
    phpVersion:
        min: 80200
        max: 80400
    paths: [app, config, database, routes]

The non-negotiable line in all of this is --fail-on-deprecation. A deprecation that does not fail the build is a deprecation nobody ever fixes.

Rollout, Observability, and Rollback

Same shape every time:

  1. CI green on the target with deprecations failing the build.
  2. Staging with production-shaped data, running the full runtime — queue workers, scheduler, Horizon — for at least 24 hours so nightly jobs actually execute.
  3. Canary one queue worker pool. Workers are the ideal canary: they exercise your heaviest code paths, and a failure degrades throughput rather than user-facing availability.
  4. Soak for a business cycle — long enough to cover a weekly report and a billing run, not an hour.
  5. Ramp web traffic 10% → 50% → 100%, watching error rate, p95 latency, and memory per worker at each step.
# Deprecations should trend to zero, not plateau
tail -f storage/logs/deprecations.log

# Version-specific failure modes
grep -iE "segmentation fault|zend_mm_heap|allowed memory size" /var/log/php-fpm.log

Add a version assertion to your health endpoint. Three lines, and it has saved me more than once from "the deploy succeeded but the FPM pool never restarted":

Route::get('/healthz', fn () => response()->json([
    'php'     => PHP_VERSION,
    'laravel' => app()->version(),
]));

For rollback: keep both runtimes installed during the canary window so reverting is an nginx upstream change and a reload. Tag the pre-upgrade release (git tag pre-php-84 && git push --tags). Drain queues before switching worker pools, since serialized job payloads written by one runtime get read by another. And keep dependency updates out of the runtime PR — if composer update on 8.4 pulls packages requiring ^8.3, your lockfile is no longer installable on 8.2 and your rollback has quietly stopped existing. Verify it with the old binary against the new lockfile:

composer check-platform-reqs

Finally: a PHP upgrade PR should touch zero migrations. The runtime is reversible. Schema is not.

If You Cannot Make December 31

Sometimes the honest answer is that sixteen weeks is not enough — a blocking package with no maintainer, a code freeze through Q4, an Octane dependency without a build. In that case, stop pretending and mitigate deliberately:

  • Get the distribution or vendor extended-support contract in writing, with its own expiry date, and record it as a compensating control.
  • Reduce the attack surface now: WAF in front, admin paths network-restricted, expose_php = Off, and no PHP-executable upload directories.
  • Set a board-visible date in Q1 2027 with a named owner. An EOL runtime with a funded, dated remediation plan is an audit finding. One without is a governance failure.
  • Consider bringing in help. If the blocker is capacity rather than complexity, that is the cheapest problem on this list to solve. The evaluation criteria in my Laravel and AWS consultant checklist will help you scope it properly.

Key Takeaways

  • PHP 8.2 reaches end of life on December 31, 2026. Active support ended December 31, 2024; after this year there are no security fixes at all.
  • PHP 8.4 leaves active support on the same date and carries security fixes until December 31, 2028. PHP 8.5 is the only version with active support past this year — aim there unless something forces 8.4.
  • Laravel 11 is fully end of life as of March 12, 2026. Laravel 12 has security fixes until February 24, 2027. If you are on 8.2 and Laravel 11, you are running two unsupported components.
  • Upgrade PHP first, then Laravel. Composer resolves php as a platform requirement, and separating the two gives you attributable failures and an independent rollback.
  • composer why-not php 8.4 is your estimate. Package compatibility, not application code, is what stalls these projects.
  • Extensions are the unbudgeted blockerimagick, Swoole for Octane, and APM agents most of all. Verify them in a real image, in CI.
  • The compliance clock is real. Unsupported runtimes fail PCI DSS patching requirements, generate SOC 2 exceptions, and stall enterprise deals.

Verify every date here against the PHP supported versions page and the Laravel support policy before you build a plan on it. Support windows shift, and a blog post is a map, not the territory.

Need This Done Before December?

I have spent 14 years in PHP and Laravel, and a fair share of it running exactly this workstream for teams who would rather not lose their Q4 to it. If you want a second opinion on sequencing, a pre-upgrade audit, or someone to run the runtime migration while your team keeps shipping features, get in touch through the contact form. Send me your current PHP version, your Laravel version, and the output of composer why-not php 8.4 — that is usually enough for a useful first answer.

If your codebase is old enough that this reads as optimistic, the characterization-testing and staged-refactor approach in my practitioner's guide to legacy codebases with Claude Code is where I would start instead.

Richard Joseph Porter - Senior PHP and Laravel Developer, author of technical articles on web development

Richard Joseph Porter

Senior Laravel Developer with 14+ years of experience building scalable web applications. Specializing in PHP, Laravel, Vue.js, and AWS cloud infrastructure. Based in Cebu, Philippines, I help businesses modernize legacy systems and build high-performance APIs.

Need Help Upgrading Your Laravel App?

I specialize in modernizing legacy Laravel applications with zero downtime. Get a free codebase audit and upgrade roadmap.

Related Articles