code quality and tooling
Compatibility Shims and Polyfills
A compatibility shim is code that helps an application work across different versions or environments. A polyfill is a common kind of shim that provides a newer function, class, or feature when the current runtime does not have it.
In PHP projects, shims and polyfills appear during PHP version upgrades, library upgrades, and work on applications that must support more than one PHP version.
A simple polyfill shape
A polyfill for a function should check whether the function already exists before defining it.
<?php
declare(strict_types=1);
if (! function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
The guard is essential. If the runtime already has the function, defining it again would cause a fatal error.
Prefer maintained polyfills
For real applications, prefer maintained Composer packages when a well-known polyfill exists. A package is more likely to handle edge cases, test coverage, and version details than a quick local helper.
Local shims can be acceptable when:
- the compatibility gap is small
- the behaviour is project-specific
- the shim is temporary
- the tests are clear
- there is a plan to remove it later
Shims can hide upgrade work
Compatibility code is useful, but it can become technical debt. If a project has upgraded to a PHP version that natively supports the feature, the shim may no longer be needed.
For example, once every supported runtime has str_starts_with(), the local polyfill can be removed.
Version checks
Sometimes code needs to branch based on a PHP version.
<?php
declare(strict_types=1);
if (PHP_VERSION_ID < 80000) {
echo 'Running on PHP before 8.0';
} else {
echo 'Running on PHP 8.0 or newer';
}
Use version checks sparingly. They can make code harder to read and test. Prefer dependency constraints and clear upgrade paths where possible.
Composer platform constraints
Composer can declare which PHP versions the project supports.
{
"require": {
"php": "^8.2"
}
}
This is often better than scattering runtime version checks throughout application code. If the project requires PHP 8.2, application code can rely on PHP 8.2 features.
Test both paths when possible
If a shim has fallback behaviour, it needs tests. For simple local compatibility helpers, test the behaviour directly.
<?php
declare(strict_types=1);
function startsWithFallback(string $haystack, string $needle): bool
{
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
}
var_dump(startsWithFallback('invoice-1001', 'invoice-'));
var_dump(startsWithFallback('receipt-1001', 'invoice-'));
// Prints:
// bool(true)
// bool(false)
The test should prove the behaviour, not only that the function exists.
Avoid changing global behaviour casually
Global polyfills can affect the whole application. Be careful with:
- defining global functions
- changing autoload order
- replacing built-in behaviour
- adding broad helper files loaded on every request
- silently changing behaviour based on PHP version
When possible, keep compatibility logic isolated and documented.
Removing shims
A good shim should have an exit path. Add a note in code or an issue that says when it can be removed.
<?php
declare(strict_types=1);
// Remove this fallback when the minimum supported PHP version is 8.0.
if (! function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
That comment explains why the shim exists and when it should disappear.
What to remember
Shims and polyfills help code survive version gaps, but they should be small, tested, guarded, and temporary when possible. Composer version constraints are often cleaner than many runtime branches.
Before moving on, make sure you can explain why function_exists() matters in a polyfill and why compatibility code should have a removal plan.
Practice
Task: Write A Starts-With Fallback
Write a small compatibility helper that behaves like str_starts_with().
Requirements
- Use strict types.
- Name the project helper
startsWithFallback(). - Return
truewhen the needle is an empty string. - Return
truewhen the haystack starts with the needle. - Return
falseotherwise. - Include a short usage example with expected output.
- Add one sentence explaining why a real global polyfill should use
function_exists().
Show solution
<?php
declare(strict_types=1);
function startsWithFallback(string $haystack, string $needle): bool
{
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
}
var_dump(startsWithFallback('invoice-1001', 'invoice-'));
var_dump(startsWithFallback('receipt-1001', 'invoice-'));
var_dump(startsWithFallback('anything', ''));
// Prints:
// bool(true)
// bool(false)
// bool(true)
A real global polyfill should use function_exists() before defining the function so it does not fatal when the PHP runtime already provides that function.