The array_first helper function breaks in PHP =< 8.3
Laravel Version
11 / 12
PHP Version
8.3.13
Database Driver & Version
No response
Description
Hello all,
The following pull request was merged yesterday: Prevent infinite loop on array_first, array_last, str_contains.
This fix uses the function array_find_key, which was introduced in PHP 8.4. This results in an Call to undefined function array_find_key error on our systems which are still running on 8.3.
Steps To Reproduce
Run the following code in PHP 8.3
array_first([
'en' => 'English',
'nl' => 'Dutch',
'it' => 'Italian',
'fr' => 'French'
], fn($language, $languageCode) => $languageCode === 'nl');
Sent PR https://github.com/laravel/helpers/pull/38 to address this issue
@erikverbeek despite the fix sent to the laravel/helpers package, please consider migrating the usage of the helpers provided by that package to the ones provided by illuminate/support.
laravel/helpers is already basically a wrapper around illuminate/support. So your project already requires the illuminate/support under the hood.
In the code snippet you sent, you could use the Illuminate\Support\Arr::first helper:
Arr::first([
'en' => 'English',
'nl' => 'Dutch',
'it' => 'Italian',
'fr' => 'French'
], fn($language, $languageCode) => $languageCode === 'nl');
Hello @erikverbeek and @crynobone
This issue may be closed in favor of: https://github.com/laravel/helpers/pull/38
@AhmedAlaa4611 https://github.com/laravel/helpers/pull/38 does not appear to address the issue, e.g. my application uses the laravel helpers array_first and array_last with PHP 8.3 (passing callbacks, etc) and since upgrading to 1.8.1 the polyfills for PHP 8.5 are used instead of laravel's helpers, causing multiple failures.
@nicekiwi that is indeed an issue for projects that have both the laravel/helpers and any symfony/polyfill-php* packages that define the same function.
To avoid any hiccups, please update to use the Illuminate\Support\Arr helper class.
Mind that any of the symfony/polyfill-php* packages are required directly by laravel/helpers, so it is not laravel/helpers itself that is causing the clash.
You can run the following command to see which package is requiring the conflicting polyfill:
composer why symfony/polyfill-php85
Laravel recently started requiring this polyfill, so the impression that a new version of laravel/helpers is causing the issue might be a coincidence.
No helper's declaration nor existence check were changes in recent versions. Just implementation was changed to avoid clashes on projects running in recent PHP versions.
@nicekiwi that is indeed an issue for projects that have both the
laravel/helpersand anysymfony/polyfill-php*packages that define the same function.To avoid any hiccups, please update to use the
Illuminate\Support\Arrhelper class.Mind that any of the
symfony/polyfill-php*packages are required directly bylaravel/helpers, so it is notlaravel/helpersitself that is causing the clash.You can run the following command to see which package is requiring the conflicting polyfill:
composer why symfony/polyfill-php85 Laravel recently started requiring this polyfill, so the impression that a new version of
laravel/helpersis causing the issue might be a coincidence.No helper's declaration nor existence check were changes in recent versions. Just implementation was changed to avoid clashes on projects running in recent PHP versions.
I've just dropped the laravel/helpers package and moved to the internal functions, I didn't realise it was a legacy polyfill itself. 🚀
I've created a fix for this issue in PR #57345.
The problem was that the code was using PHP 8.4 functions (array_find_key, array_first, array_last) which are not available in PHP 8.3 and earlier.
The fix replaces these with PHP 8.3 compatible implementations:
-
array_find_key()→ manual iteration -
array_first()→reset() -
array_last()→end()
I've tested the exact example from your issue and confirmed it works correctly now.
The problem was that the code was using PHP 8.4 functions (
array_find_key,array_first,array_last) which are not available in PHP 8.3 and earlier.
It is: Via polyfills. Which Laravel already uses. So this is not a fix
I have the same problem in Arr:first() when passing a Collection and a callback, I'm not really understading why suddenly stopped working, I've tried it on PHP8.3 (using polyfills) and PHP8.4 (without using polyfills) but still has the same crash.
array_find_key(): Argument #1 ($array) must be of type array, Illuminate\Support\Collection given, called in /var/www/html/vendor/laravel/framework/src/Illuminate/Collections/Arr.php on line 268
I do have the laravel helpers package installed but should not be relevant...
Hello @gmutinel you should not use the Laravel helpers package.
it does not change a thing (I tried), the problem lies in Polyfill84 which defines the first argument as array
if (!function_exists('array_find_key')) {
function array_find_key(array $array, callable $callback) { return p\Php84::array_find_key($array, $callback); }
}
with this line on tinker you can replicate it
Arr::first(collect(),fn($c)=> $c)
I just don't understand why up until ~2 monthis ago was working with php8.3 and now it does not, plus I do not understand why it does not work on native php8.4 (cannot debug right now)
EDIT: I think I get it, with php8.3 it was introduced here: https://github.com/laravel/framework/pull/56619
PHP8.4 has a native support for an array type on array_find_key(), the real problem lies in the fact that this was a Breaking Change since Arr::first() parameter is defined as Iterable, which is not the case anymore when passing a callback
/**
* Return the first element in an array passing a given truth test.
*
* @template TKey
* @template TValue
* @template TFirstDefault
*
* @param iterable<TKey, TValue> $array
* @param (callable(TValue, TKey): bool)|null $callback
* @param TFirstDefault|(\Closure(): TFirstDefault) $default
* @return TValue|TFirstDefault
*/
What should the correct fix be?
@gmutinel for the moment, you can call ->all() on the collection when passing it to the Arr::first() helper.
$first = Arr::first($collection->all(), $callback);
Or use the Collection@first() method, which takes a callback:
$first = $collection->first($callback);
Under the hood, Collection@first() calls Arr@first() with the collection's $item property, which is an array.
https://github.com/laravel/framework/blob/d42d6a603569a7b44744952d88b88337a2c8afed/src/Illuminate/Collections/Collection.php#L430-L433
yeah, I just fixed with a ->toArray() in the couple of places I was using it, no problem, I should not be affected by any other change but I don't think it's only related to Arr::first() (did not check), imho this BC needs to be handled somehow.
I'd use Collection@all() over Collection@toArray() if you are just after the first matching element.
Collection@all() just returns the collection's $item inner array property, while Collection@toArray() calls ->toArray() on every Arrayable element before returning.
https://github.com/laravel/framework/blob/d42d6a603569a7b44744952d88b88337a2c8afed/src/Illuminate/Collections/Traits/EnumeratesValues.php#L951-L954
That would, for example, convert all models to their array representation before returning if called on a collection of models.
that's solid advice, thanks, I'll change to that