Skip to content

Conversation

KIKOmanasijev
Copy link
Contributor

This PR complements #56703. It replaces all of the end and reset function calls with the new array_first and array_last functions from PHP 8.5 (used through Polyfil).

@@ -203,7 +203,7 @@ function data_forget(&$target, $key)
*/
function head($array)
{
return reset($array);
return array_first($array);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not exactly the same: reset() updates the internal array pointer to the first element, while array_first() does not update internal pointer. Additionally, reset() returns false if the array is empty, while array_first() returns null.

Same goes to the end() and array_last().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern about the internal pointer changing is irrelevant to most of these changes, as arrays in PHP are passed into functions as copies, and the use of reset() or end() happens, mostly, in the return statement.

I say "most of the cases," as I didn't thoroughly check every change. But regardless of being used on the return statement, all changes seem fine overall.

The internal pointer change would be relevant if arrays were explicitly being passed as references, which is not the case in any of the changes.

<?php

function byCopy($arr) { return reset($arr); }
function byReference(&$arr) { return end($arr); }

$arr = [1, 2, 3];

// move internal pointer to the second element
next($arr);

echo 'current element: ', current($arr), PHP_EOL, PHP_EOL;

echo 'result of byCopy(): ', byCopy($arr), PHP_EOL;
echo 'original array pointer should be unchanged: ', current($arr), PHP_EOL, PHP_EOL;

echo 'result of byReference(): ', byReference($arr), PHP_EOL;
echo 'original array pointer should be changed: ', current($arr), PHP_EOL;

Output:

$ php test.php 
current element: 2

result of byCopy(): 1
original array pointer should be unchanged: 2

result of byReference(): 3
original array pointer should be changed: 3

But indeed the false on an empty array case would be a breaking change for anyone already handling that outcome.

Maybe it is best to send this to master and document the change on the upgrade guide to avoid any surprises for developers handling the false on an empty array case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the explanations! I now realize that head() and last() are the only real cases that introduce breaking changes. I still think their implementation should use array_first() and array_last(), since the purpose of these helpers is simply to return the first or last element of an array. I agree though that these 2 should target the master branch, and not 12.x. I can create another PR for them.

The other changes don’t introduce breaking changes, since they dont rely on the internal pointer or the special false return value, so I believe those can safely target the 12.x branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even in cases where the array is a reference, not manipulating the array or its internal pointer would be the expected default behavior. So if anything, this actually fixes potentially non-apparent behavior. But as always, someone might rely on this and sending it to master instead might make sense here therefore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree with @shaedrich,, that explains basically I still think their implementation should use array_first() and array_last() from my previous message 😅.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation @rodrigopedra. Indeed, I checked other usages and it seems to not impact userland except for false -> null return on empty array.
While the contract is not clearly established (return mixed) in the PHPDoc, documentation states:

The head function returns the first element in the given array. If the array is empty, false will be returned:

https://laravel.com/docs/12.x/helpers#method-head

Which should be considered a minor BC break and requires doc update. It also appears that this edge case is not covered by tests.

@taylorotwell
Copy link
Member

Can't have a breaking change on the head method on a patch release. Can you fix it to return false on empty array?

@taylorotwell taylorotwell marked this pull request as draft August 22, 2025 13:40
@KIKOmanasijev
Copy link
Contributor Author

Can't have a breaking change on the head method on a patch release. Can you fix it to return false on empty array?

@taylorotwell Done it for both the head and the last methods.

@KIKOmanasijev KIKOmanasijev marked this pull request as ready for review August 22, 2025 13:51
@taylorotwell taylorotwell merged commit f73517f into laravel:12.x Aug 22, 2025
60 checks passed
@KIKOmanasijev KIKOmanasijev deleted the feat/use-new-arr-helper-functions branch August 22, 2025 14:31
@AhmedAlaa4611
Copy link
Contributor

AhmedAlaa4611 commented Aug 28, 2025

@KIKOmanasijev @IonBazan @shaedrich @rodrigopedra @taylorotwell

In this PR, reset is sometimes replaced with array_last and other times with array_first. Is this intentional, or an oversight?

@@ -369,7 +369,7 @@ public function scalar($query, $bindings = [], $useReadPdo = true)
throw new MultipleColumnsSelectedException;
}

return reset($record);
return array_last($record);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, reset is replaced with array_last?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it seems to be an oversight. I guess it should be fixed.

Especially as the method description reads as "Run a select statement and return the first column of the first row."

ping @KIKOmanasijev

@@ -507,7 +507,7 @@ public function fillForInsert(array $values)
return [];
}

if (! is_array(reset($values))) {
if (! is_array(array_first($values))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, reset is replaced with array_first?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is fine.

@@ -116,7 +116,7 @@ protected function setForeignAttributesForCreate(Model $model)
*/
public function upsert(array $values, $uniqueBy, $update = null)
{
if (! empty($values) && ! is_array(reset($values))) {
if (! empty($values) && ! is_array(array_last($values))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it also seems to be swapped. IMO it should be array_first().

@@ -3158,7 +3158,7 @@ public function soleValue($column)
{
$result = (array) $this->sole([$column]);

return reset($result);
return array_last($result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many in this file have had reset changed to array_last instead of array_first.

Is it intentional?

@rodrigopedra
Copy link
Contributor

@AhmedAlaa4611

Yes, it seems odd that some reset were changed to array_last instead of array_first.

Maybe @KIKOmanasijev can share the rationale with us or verify if it should be fixed indeed.

@KIKOmanasijev
Copy link
Contributor Author

This is definitely an oversight. I would definitely appreciate if you can create a PR and resolve this, since I am currently on a trip without my laptop.

@AhmedAlaa4611
Copy link
Contributor

@KIKOmanasijev @rodrigopedra

I will take care of this.

@rodrigopedra
Copy link
Contributor

@AhmedAlaa4611 was just writing to you asking if you would =)

You were faster than me!

@momala454
Copy link
Contributor

momala454 commented Sep 1, 2025

i think this change cause an infinite recursion on php 8.4 with

var_dump(array_last([[]]));

tested on v12.26.4

Because I have an infinite recursion since version 12.26.0

@AhmedAlaa4611
Copy link
Contributor

i think this change cause an infinite recursion on php 8.4 with

var_dump(array_last([[]]));

tested on v12.26.4

Because I have an infinite recursion since version 12.26.0

I am using PHP 8.4 and have tested it on Laravel v12.26.4 without any issues:

var_dump(array_first([[]]));
var_dump(array_last([[]]));

@rodrigopedra
Copy link
Contributor

I also cannot replicate it.

$ php artisan tinker
Psy Shell v0.12.10 (PHP 8.4.12 — cli) by Justin Hileman
> PHP_VERSION;
= "8.4.12"

> app()->version();
= "12.26.4"

> array_last([[]]);
= []

> var_dump(array_last([[]]));
array(0) {
}
= null

Can you create a public repo where we could easily replicate the issue?

@momala454
Copy link
Contributor

momala454 commented Sep 1, 2025

Are laravel helpers included in all laravel instances ? https://github.com/laravel/helpers

/**
     * Return the last element in an array passing a given truth test.
     *
     * @param  array  $array
     * @param  callable|null  $callback
     * @param  mixed  $default
     * @return mixed
     */
    function array_last($array, ?callable $callback = null, $default = null)
    {
        return Arr::last($array, $callback, $default);
    }

    public static function last($array, ?callable $callback = null, $default = null)
    {
        if (is_null($callback)) {
            return empty($array) ? value($default) : array_last($array);
        }

        return static::first(array_reverse($array, true), $callback, $default);
    }

so, array_last([]) will call last, and as callback is null, will call array_last, which call last etc ...

https://github.com/laravel/helpers/blob/master/src/helpers.php#L147
https://github.com/laravel/framework/blob/12.x/src/Illuminate/Collections/Arr.php#L285

edit: i'm using this library which uses laravel/helpers https://packagist.org/packages/thibaud-dauce/laravel-mattermost-logger

@rodrigopedra
Copy link
Contributor

rodrigopedra commented Sep 1, 2025

@momala454 I see...

The problem is, the laravel/framework requires symfony/polyfill-php85, and the latter provides an array_last function compatible with the upcoming PHP 8.5 implementation.

In your case, the thibaud-dauce/laravel-mattermost-logger requires the laravel/helpers package, and that also defines an array_last function.

Note that the laravel/helpers is not required by default with the framework.

So, depending on the autoload order, if laravel/helpers gets loaded first, then its array_last implementation gets defined first, and when symfony/polyfill-php85 its function_exists() returns false and the polyfill implementation is never defined.

Note that in PHP one cannot redefine a function.

Some solutions:

  • Ask the thibaud-dauce/laravel-mattermost-logger package's maintainer if it is possible to not use the laravel/helpers package
    • The laravel/helpers package is meant to provide old Laravel 5.8 helpers, maybe the package doesn't need it anymore
    • Or perhaps it can use a combination of symfony/polyfill-php85 and illuminate/support package, which is already required by laravel/helpers
  • Send a PR to the laravel/helpers repository, amending its array_last implementation to match the previous Arr::last() implementation before it started using array_last

I can work on a PR for laravel/helpers, but not now; it would be much later in the day (it is 12:20 PM in my timezone.)

If you feel like doing it before, be my guest. I would also check any other recursive calls that might be happening with the adoption of the symfony/polyfill-php85 package.

@momala454
Copy link
Contributor

thank you. Please go ahead with updating helpers.

@rodrigopedra
Copy link
Contributor

@momala454 PR laravel/helpers#37 sent

@rodrigopedra
Copy link
Contributor

@momala454 I also sent an MR to the thibauddauce/laravel-mattermost-logger repository.

https://gitlab.com/thibauddauce/laravel-mattermost-logger/-/merge_requests/12

@momala454
Copy link
Contributor

thanks a lot !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants