skip to Main Content

I was happy to get that thing working in PHP 7.4, but now it’s broken again in PHP 8.1.14.

The task is to sort an array of pages by their title, but in a simplified way. Leading non-letters shall be ignored when sorting.

This is a sample code:

$pages = [
    'a' => ['title' => 'A'],
    'c' => ['title' => 'C'],
    'b' => ['title' => 'B'],
    'n' => ['title' => '.N'],
];
array_multisort(
  $pages,
  SORT_ASC,
  SORT_STRING,
  array_map(fn($page) =>
    preg_replace('_^[^0-9a-z]*_', '', strtolower($page['title'])
  ), $pages)
);

echo implode(', ', array_map(fn($page) => $page['title'], $pages));

It sorts to this output in PHP 7.4, nothing in the error log:

A, B, C, .N

In PHP 8.1, I get this error instead:

ErrorException: Array to string conversion in ...index.php:30
Stack trace:
#0 [internal function]: {closure}()
#1 ...index.php(30): array_multisort()

I don’t understand what the problem is. What has been changed here for PHP 8 and what can I do to fix the code again?

Update/clarification:

In PHP 7.4, there is a "Notice" with the text "Array to string conversion". In PHP 8.1 this has been changed to a "Warning". I treat warnings as errors so it fails in my case.

That doesn’t change that PHP thinks my code is wrong and I don’t have a clue why.

4

Answers


  1. You seem to have the arrays backwards. The first array is what is actually sorted, and the second array has the same transformation applied to it.

    array_multisort(
        array_map(fn($page) => preg_replace('_^[^0-9a-z]*_', '', strtolower($page['title'])), $pages),
        SORT_ASC, SORT_STRING,
        $pages
    );
    
    Login or Signup to reply.
  2. The problem is not in the PHP code but in the way you use array_multisort().

    The third argument (array_sort_flags) tells array_multisort() how to compare the items of the first argument (the array to sort). The value SORT_STRING means "compare items as strings" but the items of $pages are not strings but arrays.

    When an array is used in string context PHP throws the warning that you saw. It used to be a notice before PHP 8, it has been (correctly) promoted to warning in PHP 8. It could even be changed to error in a future PHP version.

    Do not hide the notices, many time they reveal logical errors. PHP used to be very permissive and reported such errors as notices; since PHP 8 many of them were promoted as warnings. Repair the code to not produce errors, warnings or even notices.

    There are multiple ways to repair this code but the right one depends on your expected outcome. Reversing the order of the arrays solves the problem if the values of property title are unique after the transformation produced by array_map():

    array_multisort(
      array_map(fn($page) =>
        preg_replace('_^[^0-9a-z]*_', '', strtolower($page['title'])
      ), $pages),
      SORT_ASC,
      SORT_STRING,
      $pages
    );
    

    It still compares items from the $pages array but only when it encounters duplicate values in the first array.


    Another solution that is easier to read and understand but slightly worse performance wise is to use uasort() to sort $pages using the callback that is currently passed to array_map() to compare the elements

    $pages = [
        'a' => ['title' => 'A'],
        'c' => ['title' => 'C'],
        'b' => ['title' => 'B'],
        'n' => ['title' => '.N'],
    ];
    
    uasort($pages, fn($a, $b) => 
        preg_replace('_^[^0-9a-z]*_', '', strtolower($a['title']))
        <=>
        preg_replace('_^[^0-9a-z]*_', '', strtolower($b['title']))
    );
    
    echo implode(', ', array_map(fn($page) => $page['title'], $pages));
    

    Check it online.


    If $pages is large then calling preg_replace() and strtolower() for each comparison wastes time. A better solution is to compute the sort keys only once and keep them in a separate array and let the comparison function get them from this array:

    $keys = [];
    foreach ($pages as $p) {
        if (! isset($keys[$p['title']])) {
            $keys[$p['title']] = preg_replace('_^[^0-9a-z]*_', '', strtolower($p['title']));
        }
    }
    
    uasort($pages, fn($a, $b) => $keys[$a['title']] <=> $keys[$b['title']]);
    

    Check it online.

    Login or Signup to reply.
  3. As an alternative, you could use uasort.

    uasort($pages, function($a, $b) {
        $at = preg_replace('/[^a-z]+/sU', '', strtolower($a['title']));
        $bt = preg_replace('/[^a-z]+/sU', '', strtolower($b['title']));
        return strcmp($at, $bt);
    });
    
    Login or Signup to reply.
  4. You can spare your code from so much fluffing around if you isolate the title column data before sorting.

    I recommend only making replacements if the matched string has length. Also, you can sort case-insensitively with the SORT_FLAG_CASE so long as you also include SORT_STRING.

    Code: (Demo)

    $titles = array_column($pages, 'title');
    
    array_multisort(
        preg_replace('/^[^0-9a-z]+/i', '', $titles),
        SORT_STRING | SORT_FLAG_CASE,
        $titles
    );
    
    echo implode(', ', $titles);
    

    Bear in mind, array_multisort() will actually do what the name implies. The sorting rules described in the first set of parameters will be the first round of sorting. If there are any ties after that first round, the subsequent sets of arguments (or in this case the unaltered array of values) will be used to try to break ties. Demo

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search