-1

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.

10
  • 1
    I get notices in PHP 7.4 as well: 3v4l.org/QFdlv#v7.4.33 I think there's more code than you show us here. For instance something that catches and reports errors? Commented Mar 24, 2023 at 17:58
  • @KIKOSoftware I think I've disabled notices in my code, PHP just spills them everywhere! But at least it produces the expected sorting. Now it simply fails. I still don't know what the message means. It's a useless error message if it can't say what the problem is. Commented Mar 24, 2023 at 18:00
  • Yes, there's more code. I just retried it without, in a new file with only the shown code. I see the notice in PHP 7.4 and a warning in PHP 8.1. I treat warnings as errors, so it fails. Alright, how can I resolve the notice, now warning? What is the problem with my code?! I still don't get it. No matter the PHP versions or error handling. Commented Mar 24, 2023 at 18:04
  • 2
    The error message means that something expects a string as an argument but it receives an array. The array will be turned into the string: "array()". Resolve this problem and your code will probably also work in PHP 8. Warnings are there for a reason, ignore them at your own peril. Commented Mar 24, 2023 at 18:05
  • I cannot see that you actually retried the code. For instance, the error reports about line 30, and there's no line 30 in your code. Commented Mar 24, 2023 at 18:07

4 Answers 4

2

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
);
Sign up to request clarification or add additional context in comments.

5 Comments

The documentation seems to be incomplete here. I understood it to provide the array I wanted to sort as the first argument, then provide more arrays as sorting instructions. You say it's reversed. The documentation says nothing about it at all.
The docs on this one are not worded particularly well, but essentially the first argument is subject to a sort() operation and should be a 'simple' one-dimensional array of sort keys, and arguments 4+ are other [potentially more complex] arrays that are rearranged in the same way. I guess it could be said that the other arrays are sorted by the first.
Okay, reading it like this it makes sense. When I reverse the two array parameters, I can also leave away the sort order/flags parameters.
Yeah, I'm not sure exactly what PHP is doing behind the "array to string conversion" error, but it seems to be coercing the sort items into a form that sorts correctly. However I would venture to guess that this happens only by coincidence on both this test data and your actual data. Now that we've ironed out the parameter order the default sort mode works fine on this data.
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.

7 Comments

The expected outcome is a sorted $pages array. If changing the parameter order does that, I'm fine with it. The documentation just doesn't say anything about this parameter order. – The SORT_STRING parameter may be a leftover from earlier tries. I'll try to remove it. – Resolving PHP notices means not writing PHP anymore but Python. Every single array access (that might not exist) requires twice the code, for each nested array level. I don't need PHP if it gets as verbose as any other programming language here.
In your edit: What is <=>? Is it some kind of marker or comment to show me a difference of the two lines?
Without SORT_STRING, the sort order is anything but correct. I can't even tell what it is.
<=> is the so-called "spaceship operator". It has been introduced in PHP 7.0 to simplify the code of the callback functions used for sorting. It compares the values and returns a value that is <0 if the left operand is smaller, 0 if the operands are equal or >0 if the right operand is smaller. Exactly what the array sorting functions expect from the comparison callback.
I put <=> and its operands on separate lines because they are long. The newlines do not matter here everything can stay on a single line.
|
0

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);
});

2 Comments

Those pattern modifiers make no sense. Why did you add them?
As a point of clarity for readers, using uasort() instead of array_multisort() will have worse performance because uasort() will perform 5 function calls on each iteration.
0

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.