Platform Engineering

Moving PHP code onto a supported branch, without rewriting what already works

An engine update and a codebase that runs on it are two different problems. This is the second: the removed functions, the deprecated syntax and the extension surfaces a supported branch no longer offers, found by measurement rather than by watching the site fail, and changed on an estate that is still serving traffic.

The problem

The code is written in the dialect of the branch it is leaving

A codebase accumulates the idioms of whatever version it was written for, and they are not errors until the language withdraws them. That is what makes this work distinctive: nothing is broken when the analysis starts, the failures arrive all at once when the branch changes, and they arrive on the paths real users take rather than the paths a test suite covers. PHP 8.0 removed create_function(), each(), money_format(), get_magic_quotes_gpc() and the rest of a list that older code calls freely (php.net migration guide, read 24 October 2026); the branches above it deprecated dynamic properties and implicitly nullable parameters. None of it is exotic. All of it is invisible until a runtime update makes it fatal.

  • A runtime update succeeded, the service restarted, and pages now return 500s
  • Deprecation warnings appear on every request and are filtered out of the log
  • A theme, plugin or module has not had a release since before the current branch
  • The bootstrap calls a function that a supported branch no longer provides
  • Encoding or character-set conversion is done with functions that were removed
  • Nobody can say which parts of the codebase are ours to change and which are third-party
  • There is no test suite, so the first test of a change is production
  • The last attempt was abandoned because it was scoped as a redesign
Who this is for

The people who usually bring us this problem

A team whose runtime upgrade stalled on the code

The runtime work is agreed and the compatibility finding is what stopped it. You need the code current enough for the branch, and the boundary of what is our code and what is somebody else's settled before the work is quoted.

An agency inheriting a bespoke PHP application

The application works, nobody who wrote it is available, and there is no documentation of what it depends on. The dialect inventory is the artifact that replaces the missing author, at least for the question that matters now.

A WordPress or forum operator with a long plugin history

The core platform moved; the plugins, the theme's functions file and the child theme did not. This is where legacy PHP accumulates in a WordPress estate, and it is the part neither the core update nor the hardening pass touches.

Someone deciding between modernising and rebuilding

The rebuild is on the table and the version problem is the reason. Part of this engagement is an honest answer to whether the code is worth moving — including the answer that it is not.

What it costs

What this costs while it goes unfixed

Engineering faults are rarely confined to the engineering layer. These are the commercial consequences we see most often.

The failure arrives on one template, at the worst moment

A removed function is fatal where it is called, which is usually a path nobody exercises deliberately: a search results page, an import, a checkout step, an admin action. The runtime update passes every check that does not walk that route, and the loss is not the page — it is the conversion, order or submission that happened on it.

Deprecations are a schedule, not a warning

Each deprecation is the language announcing a removal. Code that logs deprecations on every request today is code that stops executing on a later branch, and the branches that introduced the deprecations are exactly the ones a migration is aiming at.

Legacy code caps the whole estate

One component that cannot run on a supported branch holds every other component on that branch too, including the ones that were updated this year. That is how an estate stays unsupported: not because of the platform, but because of the least maintainable thing on it.

A rewrite is a different risk, taken for a version reason

Rebuilding to escape a branch problem changes an upgrade into a replacement, and it brings back every fault the old code had already paid for. Existing behaviour is a specification, and it usually is not written down anywhere except in the code being deleted.

Abandoned components concentrate the exposure

A plugin or module with no release since before the current branch is the component least likely to have been tested on the next one, and it sits inside the request path rather than beside it.

What we do about it

Capabilities

Each of these is work we carry out, not an area we advise on.

Dialect inventory

What the codebase assumes, counted: removed functions and constructs with the file and line each appears on, the extension APIs it calls, the deprecated syntax it uses, and the split between code we can change and code we cannot. The same pass runs over the application, the themes and the plugins, because a legacy plugin is a finding and not a footnote.

Removed-function and syntax remediation

Mechanical replacement first — anonymous functions for create_function(), foreach or ArrayIterator for each(), mbstring for the removed mb-regex aliases and the encoding conversions that replaced convert_cyr_string(). Each change is small and each is verified, because a mechanical rewrite applied at scale without verification is how one wrong pattern becomes four hundred wrong lines.

Deprecation handling, before it becomes fatal

Running the current branch at the highest error reporting it will accept, which surfaces on the branch in use what the next branch will enforce. On the estates we have seen, most of it is a handful of patterns repeated across years of templates, and one of them is usually a suppression that has been hiding a real fault.

Extension and encoding surfaces

The dependencies that are not the language but move with it: mbstring's changed function signatures, iconv and mbstring choices for conversions, session and filter constants that were removed, and any bundled extension that later branches unbundled. Each is either provided, replaced, or established as a genuine blocker before the runtime change is scheduled.

Abandoned component strategy

For each third-party component with no current release, a decision with reasons: update if a maintained release runs on the target branch, replace with a maintained equivalent, isolate if it is peripheral, or accept the risk explicitly if the exposure is structural and the business decision is to carry it. We say which applies rather than leaving it to be discovered.

A test harness that did not exist

Smoke coverage over the paths that carry traffic — the routes in the site's own inventory, the forms, the authenticated actions, the imports — so the change can be verified and so the next upgrade is cheaper than this one. It is built for this project rather than installed as a framework: the goal is evidence at every step of the migration, not a test suite as an artifact.

Sequenced remediation, against a stated target branch

The removal list belongs to the target branch, so the work is planned against that target and split into batches that can each be verified independently — divided along the code's own boundaries, a plugin or a theme or a module, rather than by file count. A failing batch then has one owner, one cause and one revert, which is what keeps a migration from turning into an outage.

The change delivered with the runtime move

Remediation and the branch change land as one engagement, in one sequence, with the code changes verified on the current branch before the runtime moves and on the new branch immediately after. Doing them separately means paying for two windows and attributing the result to neither.

How we work

Engineering methodology

The sequence is deliberate. The order is usually what determines whether the work holds or has to be repeated.

  1. Measure the code before changing the runtime

    The inventory is cheap to produce and it is what turns an unbounded risk into a list. It also determines whether this is a modernisation or a replacement, which is the first decision, not the last.

  2. Mechanical first, semantic second

    Removed functions and closed extension surfaces have defined replacements and are corrected first, because they are verifiable. The judgements — a suppression hiding a fault, a component that should not be carried forward, a code path that should be deleted rather than fixed — come after, and each is decided rather than assumed.

  3. One change, one verification

    Batches small enough that a failure names the change that caused it. This is the discipline that makes the difference between a migration that pauses and one that has to be reverted wholesale on the night.

  4. Do not combine it with a redesign

    A version migration and a rearchitecture have different success conditions and fail in ways that are hard to tell apart. Where a rewrite is genuinely the right answer, it is a separate engagement with its own plan, priced and sequenced as such rather than smuggled into this one.

  5. Verify against the estate's own traffic

    The paths that carry real users, exercised before the runtime moves and after it, with the error rate and the logs read afterwards. A change that passes its tests but fails on a route nobody tested has shown the tests were wrong, which is worth knowing before the next batch.

  6. Leave the dialect baseline behind

    The inventory is kept current and the deprecated patterns are recorded where the next developer will trip over them, so the estate's position is measurable at any point instead of being rediscovered under time pressure at the next branch change.

Deliverables

What an engagement produces

Documentation is a deliverable, not an afterthought. On most of these engagements a large part of the value is a defect report precise enough for another team to act on.

Inventory

  • Removed functions and constructs, with file, line and count per pattern
  • Deprecated syntax in use, and the branch that will make it fatal
  • Extension APIs and unbundled extensions the application depends on
  • The split between code we can change and third-party components
  • What cannot be established from the code alone, named as an open question

Remediation

  • Mechanical replacements applied in verified batches
  • A disposition for every abandoned component: updated, replaced, isolated or accepted
  • Extension and encoding changes, with the resulting configuration
  • Deletions where a code path should not be carried forward, with the reason
  • Evidence per batch: what changed, what was run, what it showed

Afterwards

  • A smoke harness over the paths that carry traffic
  • The code verified on the new branch, with the error rate and logs read after the switch
  • The dialect baseline kept current, so the next upgrade starts from a measurement
  • A short note on what was deliberately left alone, and why
Under the hood

Architecture and technology

What a branch change actually removes

  • Hard removals across PHP 7.0 and 8.0 include create_function(), each(), hebrevc(), convert_cyr_string(), money_format(), ezmlm_hash(), restore_include_path(), get_magic_quotes_gpc(), get_magic_quotes_runtime(), fgetss(), and the FILTER_SANITIZE_MAGIC_QUOTES filter. Code that calls one stops executing when moved to a branch that has removed it, which is why an update can appear to succeed and then fail on one template.
  • Changes that fail silently rather than fatally: implode() no longer accepts its parameters in reverse order, and the result of sorting functions can differ when elements compare as equal. Both are in the same migration guide and neither produces an error, which is the class of change that outlives a migration and surfaces later as a data inconsistency nobody can date.
  • Deprecations that turn into removals on a later branch: dynamic properties in 8.2 unless the class opts in with the AllowDynamicProperties attribute, and implicitly nullable parameters in 8.4, where `T $a = null` should be written `?T $a = null`. The branches that introduced these are the branches a migration is aiming at.
  • Extension surfaces that moved with the language: mbstring's removed mb-regex aliases and the result-array requirement on mb_parse_str(), the constants that went with them, and any bundled extension later unbundled and published separately.

Why the ecosystem metadata cannot answer the compatibility question

  • requires_php is a floor, never a ceiling. Of the 100 most popular WordPress plugins, 93 declare a minimum and 88 of those declare 7.4 or lower (WordPress.org plugin API, page one by popularity, read 24 October 2026). A plugin declaring 7.4 may work perfectly on 8.4 and may also be the one that fails: the field says what it will install on, not what it has been tested against.
  • The platform's own statements disagree by several branches. WordPress recommends PHP 8.3 or greater while its core update API reports a minimum of 7.4 (both read 24 October 2026) — so a site can satisfy the platform, its host and its plugins while running a branch that receives no fixes.
  • Popularity is not maintenance. A plugin with millions of installs can have had no release since before the current branch, and an install count says nothing about the next branch — which is why the disposition for each component is a decision taken on its release history rather than on its ranking.
  • The consequence for planning: the compatibility position has to be measured on the codebase, not read from a table. That measurement is the inventory, and it is the reason this page starts there rather than at the runtime.

Two patterns from the PHP 8.0 removal list, in the form they appear in code written for 5.x and 7.x. Neither is a bug: both ran correctly on the branch they were written for, and both are fatal on a supported one.

// Before — removed in PHP 8.0
$rows = [];
reset($data);
while (list($key, $value) = each($data)) {
    $rows[$key] = $value;
}
$compare = create_function('$a, $b', 'return strcmp($a, $b);');
usort($rows, $compare);

// After — same behaviour, supported branch
$rows = [];
foreach ($data as $key => $value) {
    $rows[$key] = $value;
}
usort($rows, static fn($a, $b) => strcmp($a, $b));

The replacements are mechanical, which is the point: the expensive part of this work is finding every instance and verifying each batch, not deciding what the new code should look like. A codebase with hundreds of these has one or two patterns, and the inventory says how many.

Related work

Where we have done this

Engagements where this capability was the substance of the work rather than a line item.

Online communities

Spain's largest forum

Since 2019, Soludome has managed infrastructure and platform operations for one of Spain's largest online communities. The work covers server and performance optimisation, CDN architecture, load balancing and site security under sustained traffic.

Since 2019continuous engagement
Online communities

Arrse.co.uk

An established community platform had accumulated three separate problems that compounded each other: page loads that had degraded with growth, search visibility that had not kept pace with the community's authority, and a security posture that had not been revisited as the platform's profile grew. The engagement addressed all three concurrently.

4%increase in organic clicks
Online communities & specialist publishing

GuzziTech & RideMalibu

Since 2021, Soludome has managed infrastructure, site performance and technical SEO across GuzziTech and RideMalibu as a connected web estate. The engagement includes dedicated infrastructure and continuing operational ownership.

Since 2021managed infrastructure, performance and SEO
Adjacent problems

If this is not quite your problem

These overlap at the edges. Sending you to the right page is more useful than having you work it out.

The vulnerability is in PHP itself, not in the code

An engine flaw cannot be fixed by changing an application, and the two are routinely conflated in a security notice. If the question is which branch the estate is running, what the SAPI is, or how to patch it, that is the other page.

PHP version upgrade

The platform upgrade is blocked on the runtime, or the reverse

A CMS or forum release brings its own version requirements and its own code changes, and the PHP work is usually the floor underneath it. Where the platform move is the goal, that page sequences the two; this one owns the code the platform runs on.

XenForo installation and upgrade

The problem is the server rather than the code

Capacity, process management and operating-system maintenance sit underneath the runtime. Where the symptoms are load-shaped rather than code-shaped, that is the page to read first.

Server management

The estate is still on the platform it should leave

Where the legacy PHP is a legacy platform — a version of the CMS with no upgrade path — the answer is a migration rather than a code change, and the reasoning is different from the one on this page.

WordPress website migration

The code is current but the site is still exposed

Hardening is the application's own attack surface: authentication, file permissions, plugin policy. It is worth doing and it does not substitute for a runtime that is still receiving fixes.

WordPress security hardening
Client proof

What Clients Say; And Where To Verify It

Our client reviews are published on our public Google Business Profile. We reproduce them verbatim, with attribution, so you can read the original feedback and verify it directly at the source. We believe reviews should speak for themselves.

5.05 out of 5 stars15 Google reviewsReviews are hosted on our Google Business Profile.
  • 5 out of 5 stars

    Working with Soludome was a great experience! They delivered our project on time and within the agreed-upon budget, which is a huge plus.

    The team was professional, efficient, and great at communicating throughout the entire process. I highly recommend them to anyone looking for a reliable and high-quality partner.

    Maxence M. Melo
  • 5 out of 5 stars

    After working with various providers and experiencing mixed results, I can confidently say that Soludome has consistently exceeded our expectations. Their prompt responsiveness to follow-up questions and dedication to our team's needs have been exceptional.

    Nathan LaChineMale Survivor

Read all client reviews

Questions

Frequently asked

How is this different from the PHP version upgrade page?

One is the engine and one is the code. The runtime page covers the branch the estate runs, the SAPI in front of it, patching a disclosed vulnerability and the configuration posture that goes with it — none of which changes a line of application code. This page covers the code: calls to functions a supported branch removed, syntax that has been deprecated, extension surfaces that moved, component decisions and the harness that verifies the change. An engine flaw cannot be fixed in the code, and code written for PHP 5.6 does not run on a supported branch, so on most estates the honest answer is that both are needed and they are sequenced together.

Do we have to rewrite the application?

Usually not, and the page is written to avoid implying that modernisation means a rebuild. Most of what a branch change breaks has a defined, mechanical replacement: create_function() becomes an anonymous function, each() becomes foreach, the removed encoding functions have mbstring equivalents. The rewrite question comes up for components nobody maintains and for code paths that should be deleted rather than carried forward, and it is answered from the inventory rather than assumed at the start. Where a rebuild genuinely is the right call, it is a separate engagement with its own plan — changing the runtime and rearchitecting at the same time makes the cause of every failure ambiguous.

What usually turns out to be the real problem?

Third-party components, almost always. The application code tends to be a bounded list of patterns that we can count, replace and verify. The theme's functions file, a plugin that has had no release since before the current branch, and a module somebody added for one feature are where the work concentrates, because they are neither ours to fix nor safe to leave. That is why the inventory covers plugins and themes rather than only the application, and why the deliverable includes a decision for each one rather than a list of them.

Can compatibility be worked out from the plugin directory?

No, and the metadata is the reason. A plugin's declared minimum PHP version is a floor: of the 100 most popular plugins, 88 of those that declare anything declare 7.4 or lower (read 24 October 2026), which tells you what they will install on and nothing about what they have been tested against. WordPress itself recommends PHP 8.3 or greater while its core update API reports a minimum of 7.4. So the compatibility position is established by measuring the codebase and testing the components on the target branch, which is slower than reading a table and is the only version of the answer that holds.

The last attempt failed. What is different here?

The scope, usually. Attempts that fail are typically scoped as modernisations in the sense of a rebuild, or they are applied as one large change with the runtime switch, so a single fault reverts everything. This is delivered as verified batches against a stated target branch, with the code change proven on the current branch before the runtime moves. That ordering is deliberate: it turns most failures into a failing batch rather than an outage, and it keeps the question of what broke answerable.

Is there a test suite, or does it have to be built?

On most legacy estates there is not one, and building a smoke harness over the paths that carry traffic is part of the work rather than a prerequisite. It is deliberately not a test framework: it is coverage of the routes, forms and authenticated actions that matter, enough to give evidence at every step of the migration. The side benefit is that the next branch change costs less than this one, which is the same argument as keeping the runtime version visible on the other page.

What if part of the code cannot be changed?

Then it is stated as a blocker rather than worked around quietly. A component we cannot change and cannot replace can hold the estate on an unsupported branch, and the honest options are to isolate it, to accept the risk in writing, or to move the function it provides somewhere we do control. What we will not do is leave it in place while the rest of the estate moves and imply the position has been resolved — that is the situation the estate is already in, and it is why the last attempt stalled.

Bring us the problem you have not been able to fix

Describe what is happening rather than what you think the cause is. If we are not the right people for it, we will say so.