Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Polyfill Php85 Laravel Package

symfony/polyfill-php85

Symfony polyfill that brings PHP 8.5 core features to older PHP versions, including get_error_handler, get_exception_handler, NoDiscard, array_first, array_last, DelayedTargetValidation, Filter exceptions, and locale_is_right_to_left. MIT licensed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it as a runtime dependency, not a dev dependency. The package ships a Composer autoload.files bootstrap (vendor/symfony/polyfill-php85/bootstrap.php) that defines each global function/class only when it doesn't already exist — so it must be present in production.

composer require symfony/polyfill-php85

Check first whether you already have it transitively (Laravel pulls a whole family of symfony/polyfill-* via laravel/framework → symfony/*):

composer why symfony/polyfill-php85

If it's already there transitively, adding it explicitly is still worth doing — it pins the version you tested against and makes the "remove after upgrading" step obvious later.

Verify the backport actually loaded. There is no service provider, no config file, no publish step. The only proof is behaviour:

php artisan tinker --execute="dump(array_first(['a' => 1, 'b' => 2]));"

Where to look first:

  1. vendor/symfony/polyfill-php85/bootstrap.php — every symbol is guarded with function_exists() / class_exists(), so you can read exactly what is and isn't shimmed.
  2. vendor/symfony/polyfill-php85/README.md — the supported feature list is short and complete.
  3. The parent symfony/polyfill README — explains the shared conventions (global namespace, no namespacing of functions).

Your first use case should be the lowest-risk, highest-readability win: replacing array-slicing hacks in API resources, form requests, or helper classes.

// Before — mutates, or returns a one-element array you have to dereference
$first = reset($row);              // moves the internal pointer
$first = array_slice($row, 0, 1)[0]; // undefined index on an empty array

// After — non-mutating, safe on empty input
$first = array_first($row);
$last  = array_last($row);

That single change is the whole adoption story: you write PHP 8.5 code today, and on PHP 8.5 the polyfill's function_exists() guard makes it a no-op with zero overhead.

Implementation Patterns

1. Non-mutating first/last in request pipelines

array_first() / array_last() iterate without touching the internal pointer, unlike reset(), end(), and array_shift(). That makes them safe to call inside loops or on arrays you pass around by value.

class OrderResource extends JsonResource
{
    public function toArray($request): array
    {
        $lines = $this->lines->toArray(); // array_first() takes arrays, not Collections

        return [
            'id'          => $this->id,
            'first_line'  => array_first($lines),
            'last_line'   => array_last($lines),
            'line_count'  => count($lines),
        ];
    }
}

Prefer it over Arr::first($array) in hot paths — Arr::first() accepts a callback and does a framework-level call for what is a single opcode-level operation.

2. Handler introspection for debugging and audit middleware

get_error_handler() / get_exception_handler() return whatever is currently registered — useful when a package silently calls set_exception_handler() and swallows your Laravel handler:

// app/Http/Middleware/AssertHandlersIntact.php  (attach in a debug group only)
public function handle($request, Closure $next)
{
    $handler = get_exception_handler();

    if (!is_array($handler) || !str_contains($handler[0] ?? '', 'Exceptions')) {
        Log::warning('Unexpected exception handler', ['handler' => $handler]);
    }

    return $next($request);
}

Also handy in tests to assert a package restored state:

$this->assertNull(get_exception_handler(), 'a package leaked a global handler');

3. #[NoDiscard] as an intent contract

Annotate methods whose return value must not be ignored — token issuance, payment IDs, cache locks:

use App\Models\Payment;

class PaymentGateway
{
    #[\NoDiscard]
    public function charge(Payment $payment, int $amount): string
    {
        // returns an id callers must persist
    }
}

On PHP < 8.5 the attribute is inert — it is documentation plus a hook for static analysis. Wire it into your quality gate so it actually bites:

  • PHPStan/Psalm: add a custom rule (or the built-in "unused return value of a method marked with an attribute" rule in recent PHPStan) that flags discarded returns on #[\NoDiscard].
  • Code review: grep for $gateway->charge( on its own line.
  • Pint/Rector: nothing to do — the polyfill class makes the syntax valid, that's all.

4. Explicit filter failures at the request boundary

The polyfill gives you Filter\FilterException and Filter\FilterFailedException so you can adopt the class contract before the runtime can throw it:

use Filter\FilterFailedException;

class ParseAmount
{
    public function __invoke(string $raw): int
    {
        $value = filter_var($raw, FILTER_VALIDATE_INT);

        if ($value === false) {
            throw new FilterFailedException('Invalid amount: '.$raw);
        }

        return $value;
    }
}

Because these extend Exception, Laravel's Handler renders them as 500s by default. Map them deliberately:

// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(
        fn (FilterFailedException $e) => response()->json(['message' => $e->getMessage()], 422)
    );
})

The win isn't the runtime throwing — it's that catch (FilterFailedException $e) in your code keeps working unchanged the day you move to PHP 8.5.

5. Locale direction in Blade

locale_is_right_to_left() is a one-liner for RTL admin panels:

// AppServiceProvider::boot() or a View composer
View::share('dir', function_exists('locale_is_right_to_left')
    && locale_is_right_to_left(app()->getLocale()) ? 'rtl' : 'ltr');
<html dir="{{ $dir }}" lang="{{ str_replace('_', '-', app()->getLocale()) }}">

Note the function_exists() guard — see Gotchas; the polyfill only registers this function when ext-intl is loaded.

6. Stage the migration on a PHP matrix

Run the same branch through PHP 8.1 → 8.5 in CI. The polyfill means the code is identical everywhere; only the implementation differs. Any divergence in test results is a genuine polyfill/engine difference you want to find in CI, not in production.

strategy:
  matrix:
    php: ['8.1', '8.2', '8.3', '8.4', '8.5']

7. Make removal a scheduled task, not a memory test

composer.json script, so the polyfill doesn't linger for years:

"scripts": {
  "post-update-cmd": [
    "@php -r \"if (PHP_VERSION_ID >= 80500 && file_exists('vendor/symfony/polyfill-php85')) fwrite(STDERR, 'polyfill-php85 is redundant on PHP 8.5+ — consider removing.'.PHP_EOL);\""
  ]
}

Gotchas and Tips

Attributes are not enforced below PHP 8.5. #[\NoDiscard] and #[\DelayedTargetValidation] are supplied as plain attribute classes so your files parse and your IDE/static analyser sees them. The engine behaviour — warning on discarded returns, delayed target validation — only exists in 8.5. Do not ship logic that depends on enforcement; ship it as an analyser rule and a review convention.

array_first() returns null on an empty array — and that's ambiguous. You cannot distinguish "empty" from "first value is null". If that matters, guard first:

$first = $items === [] ? null : array_first($items);
// or: $exists = Arr::exists($items, array_key_first($items));

It takes arrays, not Collections or Arrayable. array_first($this->lines) on a Collection fails. Call ->toArray() or ->all(), or just use ->first() where a callback is needed — the polyfill does not change Laravel's Collection semantics.

array_first() is not array_shift(). It does not mutate, does not reindex, and does not consume. If existing code relies on the shift, swapping in array_first() silently changes behaviour in the next iteration of the loop.

locale_is_right_to_left() is conditionally registered. Per the v1.37.0 changelog, it is not defined when ext-intl is absent. Always probe with function_exists() before calling, otherwise you get a fatal on stripped-down containers (php:8.3-cli without intl is common in slim CI images).

Treat the intl/grapheme/MessageFormatter surface as younger than the core polyfills. Recent releases are dense with fixes here: grapheme_levenshtein() registration and memory use, grapheme_str_split() on PCRE 8 and PCRE < 10.44, Collator sorting, MessageFormatter integer rounding, and locale_is_right_to_left() script/language detection. Pin a recent tag, keep composer update flowing for this package, and write tests that exercise real locales and non-ASCII input — not just en.

Filter\FilterException lives in a root-level Filter namespace. A polyfill cannot intercept native filter_var(), so treat these classes as a contract you adopt now (throw/catch them yourself) rather than behaviour you inherit. Also check for collisions: if your app has classes under a top-level Filter\ namespace, verify how Composer resolves them against vendor/symfony/polyfill-php85/Resources/.

Teach PHPStan about the new functions. If your phpstan.neon pins phpVersion: 80300, it will report Function array_first not found. Either raise the analysis target:

parameters:
    phpVersion: 80500

…or add explicit stubs if you want analysis to keep flagging genuinely-unavailable syntax. Choose deliberately: a high phpVersion makes PHPStan accept code your runtime only handles because of the polyfill — so the polyfill must be a hard require dependency, never require-dev.

Octane and long-lived workers: the bootstrap file runs once per worker boot, which is fine. But get_error_handler() / get_exception_handler() reflect process-wide state — under Octane that state persists across requests. Use those functions for diagnostics on classic FPM, and be careful about asserting or mutating handlers inside request code under Octane.

Don't sprinkle function_exists() guards through app code. It defeats the point of the polyfill and quietly creates two code paths. The one legitimate guard is locale_is_right_to_left() (intl-dependent). Everywhere else, trust the require and let the package's own bootstrap do the checking.

When you finally upgrade to 8.5: remove the package, delete the reminder script, drop your PHPStan phpVersion override back to the true minimum, and delete any function_exists() shims — but only after your PHP matrix runs clean on 8.5, since polyfill implementations are not guaranteed byte-identical to native behaviour (that's exactly what the grapheme/Collator bugfix releases show).

Extension points are minimal by design. There's nothing to configure, no container bindings, no events. If you need to customise behaviour (e.g., different exception mapping or a stricter discard rule), do it in your own service layer or your static analysis config — never by forking the polyfill. Its value is that it is boring, leaf-level, and maintained on Symfony's release cadence.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource