symfony/polyfill-php84
Symfony polyfill that backports PHP 8.4 features to earlier PHP versions, including array_find, mb_trim, mb_ucfirst, fpow, bcdivmod, grapheme_str_split, the Deprecated attribute, PDO driver subclasses, and ReflectionConstant.
Installation:
composer require symfony/polyfill-php84
Laravel’s autoloader picks it up via Composer’s files/bootstrap chain—no manual require needed. Current release to pin or track is v1.43.0 (previous assessment covered v1.38.0).
First Use Case: Replace a Collection round-trip with the native array API your code will use after the PHP 8.4 upgrade:
// Before (Laravel 9.x/10.x on PHP ≤8.3)
$admin = collect($users)->first(fn ($user) => $user['role'] === 'admin');
// After (works on 8.3 today, drops the polyfill on 8.4 automatically)
$admin = array_find($users, fn ($user) => $user['role'] === 'admin');
Verify: php -r "echo PHP_VERSION;" — you should still be on ≤8.3, yet array_find() resolves.
Where to Look First:
array_find, array_find_key, array_all, array_any (replace collect()->first() / ->contains() loops).mb_trim, mb_lcfirst, mb_ucfirst — the v1.43.0 release re-aligns mb_trim() with native semantics, so behaviour now matches the 8.4 core function exactly.grapheme_str_split, grapheme_levenshtein, Normalizer — argument validation and ill-formed UTF-8 handling were fixed in v1.43.0 to mirror the intl extension.bcdivmod and friends now validate numeric strings the same way ext-bcmath does.#[Deprecated] for aligning custom classes with Laravel’s deprecation practices.fpow, request_parse_body, http_get_last_response_headers, odbc_connection_string_should_quote(), and the UUID constants (all aligned with native in v1.43.0).Array Operations:
$user = array_find($rows, fn ($u) => $u['role'] === 'admin');
$index = array_find_key($rows, fn ($u) => $u['role'] === 'admin');
$allActive = array_all($users, fn ($u) => $u['active']);
$anyAdmin = array_any($users, fn ($u) => $u['role'] === 'admin');
Multibyte String Handling (now native-identical as of v1.43.0):
$cleanInput = mb_trim($request->input('name', '')); // Unicode whitespace, emoji-safe
$title = mb_ucfirst($request->input('title', '')); // correct for non-ASCII
$slugish = mb_lcfirst($request->input('title', ''));
Grapheme-Safe User Input (validated like intl since v1.43.0):
$chars = grapheme_str_split($request->input('nickname', '')); // clusters, not bytes
$distance = grapheme_levenshtein($a, $b); // typo-tolerance for CJK/emoji
$normalized = Normalizer::normalize($request->input('name', ''), Normalizer::FORM_C);
if ($normalized === false) {
// ill-formed UTF-8 — same contract as ext-intl
}
Deprecation Management:
#[Deprecated('Use App\Services\V2\LegacyService instead')]
class LegacyService { /* ... */ }
Combine with Laravel’s own deprecation surface for a single warning channel.
Math/Precision:
[$quotient, $remainder] = bcdivmod('10.5', '3', 4); // ['3.5000', '0.0000']
$power = fpow(2, -3); // 0.125 (negative exponents supported)
BCMath arguments are now validated like the native extension—non-numeric strings produce the same ValueError you’ll get after upgrading.
PDO and cURL:
$pdo->setAttribute(PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT, true);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
Str::of()->trim() for mb_trim() in multilingual validation:
'name' => ['required', fn ($attr, $value) => mb_strlen(mb_trim($value)) > 2],
Unicode normalization is now a realistic option for search indexes given the v1.43.0 Normalizer alignment.Normalizer::normalize(), non-string args to grapheme_str_split() / grapheme_levenshtein(), junk numeric strings to bcadd()/bcdivmod().mb_trim in FormRequests with emoji and CJK input.array_find vs collect()->first() on hot paths.- name: Install dependencies
run: composer require "symfony/polyfill-php84:^1.43" --no-interaction
array_find for flat arrays; keep Model::query()->first() for anything hitting the database.null-vs-false contract:
$this->app->bind('array_finder', fn () => new class {
public function find(array $items, callable $callback): mixed
{
return array_find($items, $callback);
}
});
@php blocks (or better, view models) rather than inline conditionals.array_all($rows, fn ($r) => filled($r['email'])) reads better than a foreach with an early return.defined() guard.null and mb_trim:
mb_trim() with the core function. On PHP ≤8.3 the polyfill's contract (rather than an error) governs what you get—historically TypeError before v1.38.0, deprecation-style handling after.mb_trim($value ?? ''). Don't rely on polyfill-specific error behaviour that can shift between patch releases.Ill-formed UTF-8 with Normalizer / grapheme functions:
false where you expected a string.$value = $request->input('name');
if (! mb_check_encoding($value ?? '', 'UTF-8')) {
throw ValidationException::withMessages(['name' => 'Invalid encoding']);
}
Grapheme argument validation:
grapheme_str_split() and grapheme_levenshtein() now validate arguments the way the extension does (v1.43.0). Calls that silently coerced previously will now throw.grapheme_str_split((string) $value)—and add a test with non-string input.BCMath strictness:
' 1.0', '1e3', or '' are rejected like native, so lenient legacy input paths break.trim() + is_numeric() before BCMath calls; store decimals as strings in the DB, not floats.array_find returns null, not false:
0, '', false, and null all fail a naive if (!$result).array_find_key when null is itself a legitimate stored value:
$key = array_find_key($items, $callback);
$found = $key !== null;
PCRE version prerequisites:
grapheme_str_split / \X matching needs PCRE ≥10.44 on some shared hosts.$clusters = @grapheme_str_split($s) ?: preg_split('/\X/u', $s);
PDO driver subclasses / ODBC quoting:
odbc_connection_string_should_quote() was realigned in v1.43.0—if you copy-pasted its earlier (divergent) output into config, regenerate it.PDO::getAvailableDrivers() and PDO::getAttribute(PDO::ATTR_DRIVER_NAME) in an integration test.#[Deprecated] on old runtimes:
Performance:
composer show symfony/polyfill-php84
Check the version is ≥1.43.0 before trusting the alignment fixes.grep -r "function array_find" app/ — a userland definition wins over the polyfill and reintroduces divergence.mb_check_encoding($input, 'UTF-8') and PCRE_VERSION in a middleware when grapheme/normalizer output looks wrong.error_reporting(E_ALL) in local/CI, otherwise #[Deprecated] silently passes.require it manually—you'll double-register constants on some platforms.require:
if (PHP_VERSION_ID < 80400) {
require __DIR__.'/../vendor/symfony/polyfill-php84/bootstrap.php';
}
In practice Composer already does this—treat the snippet as a debugging tool, not a default."scripts": {
"post-install-cmd": ["@php artisan polyfill:doctor"]
}
// app/Console/Commands/PolyfillDoctor.php (register in routes/console.php or via withCommands)
$this->line('PHP: '.PHP_VERSION.' | PCRE: '.PCRE_VERSION);
$this->line('polyfill-php84: '.(\Composer\InstalledVersions::getPrettyVersion('symfony/polyfill-php84') ?? 'missing'));
^1.43 in composer.json so a composer update can't leave you on a pre-alignment release that behaves differently from production PHP 8.4.How can I help you explore Laravel packages today?