symfony/polyfill-php86
Symfony polyfill that backports PHP 8.6 features to older PHP versions, including clamp(), the ARRAY_FILTER_USE_VALUE constant, the SortDirection enum, and grapheme_strrev. A lightweight MIT-licensed compatibility layer for existing projects.
The package is a library, not a Laravel package: no service provider, no facade, no publishable config. composer require is the entire setup, because Symfony polyfills register themselves through Composer's files autoload and gate every definition behind function_exists() / define() / enum_exists() checks. Nothing runs unless PHP is older than 8.6, in which case the global symbols simply appear.
composer require symfony/polyfill-php86:^1.43
Pin deliberately rather than using ^1.0: the feature set is cumulative across split-repo tags — clamp() arrived in v1.34, SortDirection and ARRAY_FILTER_USE_VALUE in v1.35, grapheme_strrev() in v1.37, a UTF-8 correctness fix in v1.38, and further grapheme_strrev() fixes in v1.43 (bugs #661 and #695). Anyone who pinned ^1.38 because that was "the version where grapheme_strrev got fixed" should move to ^1.43 — the function has needed more than one pass.
Where to look first. Read vendor/symfony/polyfill-php86/bootstrap.php (and its Resources/stubs directory). It's a few dozen lines and it is the authoritative answer to "what exactly got defined on my runtime" — more useful than the README, which only links to the monorepo. Then the linked RFCs for exact semantics; the polyfill aims to match them, not to define its own contract.
First use case. A pagination guard. This is the single most common place a clamp belongs in a Laravel app and it exercises the feature end-to-end:
// app/Http/Requests/IndexUsersRequest.php
protected function prepareForValidation(): void
{
$this->merge(['per_page' => clamp((int) $this->input('per_page', 15), 1, 100)]);
}
Confirm what you actually got before writing code against it:
php -r 'var_dump(function_exists("clamp"), defined("ARRAY_FILTER_USE_VALUE"), enum_exists("SortDirection"));'
Call the global functions directly. There is no wrapper class to inject and no reason to wrap one. Every polyfilled symbol lives in the global namespace, so inside App\ namespaced code an unqualified clamp(...) resolves via PHP's global fallback — but writing \clamp(...) (or importing use function clamp;) makes the dependency visible in review and avoids a future namespaced collision.
Use clamp() at the input boundary, not deep in the domain. The value is in replacing hand-rolled ternaries at the point where untrusted data becomes typed data — form requests, DTO constructors, config readers, job payload hydration:
$rating = clamp($dto->rating, 1, 5);
$discount = clamp($discount, 0.0, 0.5);
$page = clamp($request->integer('page', 1), 1, $lastPage);
The min > max ordering check and NAN handling live inside the function, which is exactly the part hand-rolled versions get wrong. These paths have been stable since v1.34, so clamp() is the low-risk part of the package.
SortDirection belongs at the API edge, not in the query builder. Model it as a typed request parameter, then translate it to what Laravel's builder actually accepts — orderBy() wants the string 'asc' / 'desc', not an enum case:
$direction = SortDirection::from($request->input('direction', 'asc')) ?? SortDirection::Ascending;
$query->orderBy('created_at', match ($direction) {
SortDirection::Ascending => 'asc',
SortDirection::Descending => 'desc',
});
Keeping the enum out of Eloquent means swapping the polyfill for the native enum later touches one mapping function. Note that the v1.43 changelog's "Collator sorting" fix is not this enum — it belongs to the intl family of polyfills and does not change SortDirection behaviour here.
ARRAY_FILTER_USE_VALUE is documentation, not behaviour. It exists so array_filter($a, $cb, $flag) reads symmetrically with the USE_KEY / USE_BOTH flags. Its value is the default (0), so adopting it changes no output — write it where the explicitness helps a reviewer and skip it otherwise.
grapheme_strrev() for user-visible strings only. It reverses by grapheme cluster, so combining marks and emoji stay intact where strrev() would shatter them. Reach for it in display formatting (reversed labels for RTL layouts, mirroring in generated captions), never in identifiers, hashes, or protocol strings. Given two rounds of upstream bug fixes, keep this the only place you use it — a single helper wrapping it means the next fix is a composer bump rather than a code hunt.
Test both paths in CI. Add a PHP 8.6 (or nightly) job alongside your current version. Tests written against the polyfill only prove the polyfill works; the point is that your call sites behave identically when the guard flips to native. Assert boundary values explicitly — clamp(5, 10, 20) === 10, clamp(25, 10, 20) === 20, clamp(NAN, 0, 1), clamp(-0.0, 0.0, 1.0), numeric strings, and the min > max case. For grapheme_strrev(), pin down the exact cases that upstream has been churning on: family emoji (ZWJ sequences), regional-indicator flags, skin-tone modifiers, single combining marks ("e\u{0301}"), and mixed-script strings. Those assertions are what will tell you whether a future polyfill bump silently changed rendered output.
Keep static analysis honest. On PHP < 8.6, PHPStan and Psalm do not know these symbols exist and will report undefined function/class/constant. Provide a small stub file declaring the signatures and point phpstan.neon at it (stubFiles), or baseline the specific errors per file — but scope the baseline to files that legitimately use PHP 8.6 syntax so it can be deleted in one pass after the upgrade. Do the same for PhpStorm via a .phpstorm.meta.php or an IDE-helper stub if autocompletion matters to your team.
use Symfony\Polyfill\Php86\clamp;. The namespaced implementation classes exist for internal wiring; the public surface is the global function. Importing the namespaced one gives you a different symbol that ignores the guard.grapheme_strrev() fix train is not finished. v1.38 fixed UTF-8 handling; v1.43 ships additional fixes under bugs #661 and #695. If grapheme_strrev() output is user-visible, treat any 1.x bump as a potential rendering change and re-run your grapheme fixtures rather than merging the dependency bump blind.SortDirection or any php86 symbol changed. Verify against bootstrap.php / the stubs, not the release notes.polyfill-php80 / polyfill-mbstring / polyfill-ctype, nothing pulls this in transitively — Laravel and Symfony do not require it. If you depend on clamp() without require-ing the package, the failure surfaces only on PHP < 8.6, only when that code path executes, and only in production if your dev box happens to be newer. Add it to require (not require-dev) the moment app code calls it.files autoload entries before your app/helpers.php. If you already ship a global clamp() with a different signature, the polyfill defines first and your helper fatals. Delete the hand-rolled helper when you adopt the polyfill.config.platform.php in composer.json does not disable the polyfill. The bootstrap checks the real PHP_VERSION_ID constant at runtime, so emulating 8.6 for dependency resolution still leaves the polyfill active on your 8.3 machine. Only an actual runtime upgrade turns it off.clamp() raises ValueError when min > max. That is correct RFC behaviour, not a bug — validate the bounds (e.g. from a settings table or admin input) before clamping with them, or catch it at the boundary rather than letting it 500.ARRAY_FILTER_USE_VALUE === 0. Identical to passing no flag. If you see a diff where someone added it and expected different filtering, the change they needed was ARRAY_FILTER_USE_KEY (or Collection::filter with a key-aware callback).grapheme_strrev() returns false on invalid UTF-8. Never pass the result straight into a template — false renders as ''. Guard with mb_check_encoding($input, 'UTF-8') and decide whether to fall back to the original string or reject it. Keep this guard even after v1.43: the function's error contract has moved once already.symfony/polyfill-intl-grapheme needs version alignment, not just a one-time check. Because the v1.43 fixes span both the php86 and intl changelogs, running polyfill-php86:^1.38 next to polyfill-intl-grapheme:^1.43 (or vice versa) can produce behaviour that differs by which one wins the function_exists() race. Verify with php -r 'print_r(new ReflectionFunction("grapheme_strrev"));' after every bump and keep both constraints moving together.symfony/polyfill-* into one PR rather than fighting weekly churn — but let the grapheme_strrev fixtures run on that PR.min(max(...)) there.// PHP 8.6: replace with native clamp()) and audit with composer why symfony/polyfill-php86 plus a grep -r 'clamp(\|SortDirection\|ARRAY_FILTER_USE_VALUE\|grapheme_strrev' app/ before removing on upgrade. The polyfill stays inert on 8.6, so leaving it installed is harmless — but it hides the fact that you forgot to clean up, and it keeps you subscribed to the release churn.How can I help you explore Laravel packages today?