phplaravellaravel-localizationphp-8

Laravel trans_choice not working after PHP 8.0 upgrade


I have the following assertion in a feature test:

// just a convenience method to post a CSV file
$this->importData($postdata, $csv)
    ->assertStatus(200)
    ->assertExactJson([
        "alert" => null,
        // response text copied from RoomController::import()
        "message" => sprintf(__("%d items were created or updated."), count($csv_data)),
    ]);

This passes without issue in PHP 7.4. Without making any changes to my application code, I updated to PHP 8.0 and am now presented with:

  Failed asserting that two strings are equal.
  --- Expected
  +++ Actual
  @@ @@
  -'{"alert":null,"message":"2 items were created or updated."}'
  +'{"alert":null,"message":"2 item was created or updated."}'

The controller code in question looks like this:

if ($errcount === 0) {
    $response_code = 200;
    $msg = sprintf(
        trans_choice(
            "{0}No items were created or updated.|{1}%d item was created or updated.|{2,}%d items were created or updated.",
            $count
        ),
        $count
    );
} else {
    // some other stuff
}
return response()->json(["message" => $msg, "alert" => $alert], $response_code);

So my problem is that trans_choice is for some reason returning the singular item in PHP 8.0.

I can find no explanation for why this might be happening. Reverting back to PHP 7.4, everything passes again so it's definitely tied to the PHP version. Troubleshooting is difficult because when I fire up artisan tinker and do echo trans_choice("{0}foo|{1}bar|{2,}baz", 3); I always get "bar" as a result, whether I'm using PHP 7.4 or 8.0.

Locale should not come into this, since I'm using raw strings, but for the record both locale and locale_fallback in config/app.php are set to "en".


Solution

  • Okay, after plenty of dump and dd I was able to trace the different behaviour to Illuminate\Translation\MessageSelector::extractFromString(), and also realize that I'm using the wrong syntax.

    That method is doing some regex followed by a loose comparison between the condition and the value. The only reason it worked in PHP 7.4 was because the condition "2," is loosely equal to 2. Comparing strings to integers is done in a more reasonable fashion in 8.0, so the method returns null in all cases, and the singular default is used.

    However, instead of using regex syntax {2,}, I should have been defining my string like this:

    "{0}No items were created or updated.|{1}%d item was created or updated.|{2,*}%d items were created or updated."
    

    The asterisk is detected by the function and short circuits the return to give the proper value. Had I been testing any value other than 0, 1, or 2 my tests would not have passed in any version of PHP.