Anonymous function content in PHP not working

I’ve got a website running on a pretty old PHP 5.6.38 installation… and I’ve now taken the plunge to move it onto the latest XAMPP version (with PHP 8.0.3).

Unsurprisingly, there’s a few changes necessary, but the one I can’t seem to get sorted is the one relating to the deprecated “create_function” function. I used this to allow me to dynamically sort associative arrays by one or more key names… for example :

usort($myarray, create_function('$a,$b', get_usort_function('field2 ASC, field5 ASC')));

Now, I’ve read that I should be using an anonymous function, so have changed the code to be as follows…

usort($myarray, function($a,$b) { get_usort_function('field2 ASC, field5 ASC'); } );

The get_usort_function is used to create the text required for the comparisons – so for the example above it would return something like…

$field2=compare_ints($a['field2'], $b['field2']); if($field2==0){return compare_ints($a['field5'], $b['field5']);}else{return $field2;}

Now, in the PHP8 version then the anonymous function isn’t working – BUT if I hardcode the string that get_usort_function returns then it DOES work. Am I missing something?

A simplified example of this in action is as follows…

<?php

function compare_ints($val1, $val2)
{
    return $val1 <=> $val2;
}

function dynamic_create_usort_function()
{
    $str='return compare_ints($a[' . "'" . 'id' . "'" . '], $b[' . "'" . 'id' . "'" . ']);';
    
    return $str;
}

$a1 = array( 'id' => 9, 'name' => 'Andy');
$a2 = array( 'id' => 5, 'name' => 'Bob');
$a = array($a1, $a2);

$s = dynamic_create_usort_function();

print "nn***$s***nn";

print_r($a);

usort($a, function($a,$b) { dynamic_create_usort_function(); } );

print_r($a);

usort($a, function($a,$b) { return compare_ints($a['id'], $b['id']); } );

print_r($a);

?>

The above example gives output of…

***return compare_ints($a['id'], $b['id']);***

Array
(
    [0] => Array
        (
            [id] => 9
            [name] => Andy
        )

    [1] => Array
        (
            [id] => 5
            [name] => Bob
        )

)
Array
(
    [0] => Array
        (
            [id] => 9
            [name] => Andy
        )

    [1] => Array
        (
            [id] => 5
            [name] => Bob
        )

)
Array
(
    [0] => Array
        (
            [id] => 5
            [name] => Bob
        )

    [1] => Array
        (
            [id] => 9
            [name] => Andy
        )

)

I’d really like to solve this as my website makes a lot of use of this usort function! So, obviously the least amount of rework that’ll be needed is the dream…

Thanks in advance, Darren

Answer

Issue

create_function takes two strings, the arguments and body of the function to create. Internally, it uses eval to create something callable, which is generally frowned upon, as it increases the attack surface area.

From your description, the get_usort_function function returns a string; as you noted, if called like:

get_usort_function('field2 ASC, field5 ASC')

it would return something like:

$field2=compare_ints($a['field2'], $b['field2']); if($field2==0){return compare_ints($a['field5'], $b['field5']);}else{return $field2;}

You’d noted that hardcoding the string in the callable passed to usort works, which I’m imagining is something like:

usort($myarray, function($a,$b) { $field2=compare_ints($a['field2'], $b['field2']); if($field2==0){return compare_ints($a['field5'], $b['field5']);}else{return $field2;} } );

but a more accurate hardcoding, given the above description of how get_usort_function works, would be:

usort($myarray, function($a,$b) { "$field2=compare_ints($a['field2'], $b['field2']); if($field2==0){return compare_ints($a['field5'], $b['field5']);}else{return $field2;}" } );

When written out like this, it’s clear that changing the usort invocation from create_function to using a callable as you’ve indicated above won’t return anything (so usort will leave all the elements in their existing order). This may be the part that wasn’t clear in your understanding of how it was working.

Solution

You could do something like the following (which may be similar to a simplified version of the internal logic of the existing get_usort_function):

<?php

function print_people($people) {
  foreach($people as ['id' => $id, 'name' => $name]) {
    print("{$id}: {$name}n");
  }
  print("n");
}

function get_usort_callable(...$fields) {
  return function ($a, $b) use ($fields) {
    foreach ($fields as $field) {
      $result = $a[$field] <=> $b[$field];
      if ($result != 0) { return $result; }
    }
    return 0;
  };
}

$a1 = ['id' => 9, 'name' => 'Andy'];
$a2 = ['id' => 6, 'name' => 'Carol'];
$a3 = ['id' => 5, 'name' => 'Bob'];
$a = [$a1, $a2, $a3];

print_people($a);

usort($a, get_usort_callable('id', 'name'));

print_people($a);

usort($a, get_usort_callable('name', 'id'));

print_people($a);

which gives the following output:

9: Andy
6: Carol
5: Bob

5: Bob
6: Carol
9: Andy

9: Andy
5: Bob
6: Carol

The main takeaway here is using the use keyword on the anonymous function returned by get_usort_callable to make $fields available for use. If you wanted to match the functionality of the existing get_usort_function you could rewrite it to take a string, and split that up.

Minimal work

Given that you are going to have to move away from create_function to use PHP 8, the least amount of work you can do will be to rewrite get_usort_function to return a callable (similar to above) and replace invocations like:

usort($myarray, create_function('$a,$b', get_usort_function(...)));

with:

usort($myarray, get_usort_function(...));

Given that you have access to the internal logic of get_usort_function it should not be too difficult, and thankfully that is only in one spot. The refactoring of the call-sites is pretty mechanical too, almost a find and replace with any IDE.

Going forward (and depending on your preference), you may want to replace the SQL ORDER BY style string with a structured array, eg:

'field2 ASC, field5 ASC'

becomes:

[
  [
    'field' => 'field2',
    'direction' => 'asc'
  ],
  [
    'field' => 'field5',
    'direction' => 'asc'
  ]
]

to avoid possible issues with whitespacing etc that require additional handling in get_usort_function.

create_function vs anonymous functions

When moving from uses of create_function to using anonymous functions, you no longer need to have the arguments and body of the function passed as strings. For example, the following two callables are equivalent:

create_function('$a,$b', 'return $a["id"] <=> $b["id"];')
function($a, $b) { return $a["id"] <=> $b["id"]; }

and could be used interchangeably.

Adding in one level of indirection (as you have in your question), through use of the following functions:

function sort_by_function_body($field) {
  return "return $a["{$field}"] <=> $b["{$field}"];";
}

function sort_by_callable($field) {
  return function($a, $b) use ($field) { return $a[$field] <=> $b[$field]; };
}

these two are also equivalent:

create_function('$a,$b', sort_by_function_body('id'))
sort_by_callable('id')

The main takeaway from this is that the sort_by_callable function itself returns a specialised anonymous function, which will sort on the field passed in, as opposed to a string containing the code to perform the same logic.