symfony/polyfill-php81
Symfony polyfill providing PHP 8.1 features on older PHP versions: array_is_list, enum_exists, ReturnTypeWillChange, MYSQLI_REFRESH_REPLICA, and CURLStringFile.
Check before you install. This polyfill is pulled in transitively by a large slice of the Symfony/Laravel ecosystem, so on a typical Laravel 8/9 app it is already in your lock file:
composer show symfony/polyfill-php81
composer why symfony/polyfill-php81 # who's pulling it in, and at what version
If it's already there, you have nothing to do — the functions are already registered and callable. If you want to depend on it in your own code (rather than piggyback on a transitive version), add it to require:
composer require symfony/polyfill-php81:^1.31
composer dump-autoload
There is no integration surface. No service provider, no facade, no config/polyfill.php to publish. The package uses Composer's files autoload to evaluate a bootstrap script at autoload time, and that script declares the missing globals in the global namespace only if PHP is older than 8.1 and the symbol doesn't already exist. That means:
array_is_list([1, 2, 3]); // works on 7.4 — the global function is shimmed
You do not call Symfony\Polyfill\Php81\Php81::array_is_list() — that class is an internal implementation detail and the public contract is the native global name. Write your code against the native PHP 8.1 API and let the polyfill be invisible; that way the day you bump php in composer.json, you delete nothing.
Verify the shim is live (useful in a tinker session or a php -r on the deploy target):
php -r "require 'vendor/autoload.php'; var_dump(PHP_VERSION_ID, function_exists('array_is_list'));"
First use case worth doing today: normalizing arrays at your API boundary. array_is_list() answers "will this encode as a JSON array or a JSON object?", which is the single most common place Laravel devs hand-roll array_keys($a) === range(0, count($a) - 1) and get it subtly wrong.
// app/Http/Resources/Concerns/NormalizesLists.php
public function toArray($request): array
{
$data = parent::toArray($request);
return array_map(
fn ($value) => is_array($value) && ! array_is_list($value)
? $value // keep as JSON object
: $value,
$data
);
}
(The realistic version of this is $value = array_is_list($v) ? $v : (object) $v; before returning — the point is array_is_list is the decision predicate.)
1. Guard clause at the boundary, native call inside. Don't sprinkle function_exists() checks through your codebase. Pick one place per feature:
// app/Support/Arr.php
public static function isList(array $value): bool
{
return \array_is_list($value); // polyfilled on 7.4, native on 8.1+
}
That wrapper is also your swap point on upgrade day — you can inline it away later with a single find-and-replace, or keep it as documentation of intent.
2. #[ReturnTypeWillChange] when you implement SPL interfaces. This is the most under-rated part of the package and the one you'll hit when writing custom collection/session/iterator classes in a package that must support 7.4 and 8.1 simultaneously. PHP 8.1 added tentative return types to ArrayAccess, Countable, IteratorAggregate, JsonSerializable, SessionHandlerInterface, etc. Without the attribute you get deprecation noise on 8.1 if you omit the return type:
use ReturnTypeWillChange;
class SettingsBag implements \ArrayAccess, \Countable
{
#[ReturnTypeWillChange]
public function offsetGet($offset) { /* legacy signature kept */ }
#[ReturnTypeWillChange]
public function count() { /* ... */ }
}
On PHP 8.1 the attribute is native; on 8.0 the polyfill declares the attribute class; on 7.4 #[...] is parsed as a # comment — which is why it must sit on its own line (see Gotchas).
3. enum_exists() as feature detection, never as an enum. Use it in library code that needs to inspect a class:
// In a serializer / validation rule that accepts "enum-like" classes
if (enum_exists($class)) {
return $enum::cases(); // native path
}
return (new \ReflectionClass($class))->getConstants(); // 7.4 path
Remember the polyfill returns false on <8.1 — always pair it with a fallback branch. Do not use it to gate your own features; use PHP_VERSION_ID >= 80100 for that.
4. Package/library authoring. If you maintain a Laravel package that calls array_is_list() and you support 7.4, declare symfony/polyfill-php81 in require (not require-dev). Libraries that depend on the host app happening to have a shim get caught out the moment someone installs them into a minimal project.
5. CI matrix. Run your suite on both your minimum (7.4/8.0) and 8.1+. The whole value of the polyfill is behavior parity; a green build on 8.1 and a green build on 7.4 is the only proof you have it.
6. CURLStringFile for multipart uploads without touching disk. Laravel's HTTP client (Http::attach()) handles this for you on modern Guzzle, but if you're building a raw curl call or a Guzzle option array manually:
$payload = ['file' => new \CURLStringFile($bytes, 'report.pdf', 'application/pdf')];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
Requires PHP >= 7.4 and libcurl >= 7.56 — both satisfied on any Laravel 8+ host.
7. Treat it as removable. Nothing in your app should use Symfony\Polyfill\Php81\.... When you land on PHP 8.1+, the correct migration is composer remove symfony/polyfill-php81 (or just let the transitive dep disappear) and change zero lines of application code.
The README oversells enums. "Enum support on 7.4" is not what this does. enum is language syntax — no polyfill can backport enum Suit { case Hearts; }. All you get is enum_exists(). If you actually want enum semantics on 7.4, reach for myclabs/php-enum or spatie/enum (class constants), and plan to rewrite to native enums at 8.1. Ditto readonly, never, first-class callable syntax, fibers, pure intersection types, and ReflectionClass::isEnum() — none are polyfilled, and calling isEnum() on 7.4 is a fatal error, not a false.
array_is_list() is about keys, and PHP hands you non-list arrays constantly. array_filter, array_unique, array_diff, unset($a[0]), and json_decode($json) (with an object payload) all produce sequential-looking arrays that are not lists. If you're normalizing for JSON, the fix is array_values() before encoding, and array_is_list() is just the check that tells you it's needed. Note array_is_list([]) === true — empty is a list — so it will not catch "empty array that should have been {}".
Don't mechanically invert Arr::isAssoc(). It's a different helper with its own edge-case semantics and it isn't deprecated. Swap deliberately and cover the swap with a test.
Performance is not free, but it is small — and array-size dependent. The polyfill is a PHP-level loop; native is C. For a 5-element request payload it's noise. For a 50k-element collection inside a queued job loop, it's measurable. If a hot path matters, benchmark on 7.4 and 8.1 (Blackfire/Xdebug/phpbench), and if it hurts, hoist the check out of the loop or branch on PHP_VERSION_ID.
#[ReturnTypeWillChange] on its own line, always. On PHP < 8.0 the # starts a line comment, so public function count() { } #[ReturnTypeWillChange] silently eats the rest of the line. Worse, if you write #[ReturnTypeWillChange] public function count() all on one line, PHP 7.4 comments out the entire method declaration and you get a confusing "class must implement Countable" fatal. Put attributes above the signature.
Collision order matters. The bootstrap guards with function_exists(), so if another package (or a legacy app/helpers.php) defines array_is_list() in the global namespace, whoever loads first wins and the other is skipped — but only if that package also guards. A non-guarded function array_is_list() in your own composer.json files is a hard fatal "cannot redeclare". Grep your helper files before adding a shim of your own. Symfony's Php81 class is @internal; treat it as such.
Static analysis won't see the shim. PHPStan/Psalm validate against the phpVersion you configure, not against Composer's files autoload. If you analyse at php: 7.4, array_is_list() reports "function not found". Two workable answers: add a tiny stub file listing the polyfilled symbols to stubFiles/scanFiles, or run analysis on the maximum version and configure a separate rule set for minimum-version compatibility (e.g. phpstan/phpstan + phpVersion at 8.1 but a PHPCompatibility sniff at 7.4). Doing nothing means your IDE is red-squiggling correct code and your CI is either red or wrongly green.
MYSQLI_REFRESH_REPLICA is essentially dead weight in Laravel. Laravel speaks PDO; this constant only matters for raw mysqli tooling. It's defined by the bootstrap unconditionally, which is harmless but means defined('MYSQLI_REFRESH_REPLICA') is not a reliable "mysqli is installed" check.
Don't pin the version you inherit transitively. The package ships frequent no-op releases ("no significant changes") aligned to Symfony's release train; the only real churn recently was additional cURL constants. If it's transitive, let composer update move it. Only pin if you've added it to require yourself, and even then a caret on the 1.x line is enough.
It is not a security story. PHP 7.4/8.0 are EOL; the polyfill buys you feature parity, not patches. If you find yourself writing "we're staying on 7.4 because we have this polyfill," that's the failure mode — the polyfill's job is to unblock incremental migration while the upgrade ticket is already open.
How can I help you explore Laravel packages today?