PHP中的匿名函数内容无效

3
我已经有一个运行在相当老的PHP 5.6.38版本上的网站...现在我已经决定将其迁移到最新的XAMPP版本(带有PHP 8.0.3)。
毫不意外,有一些必要的更改,但我似乎无法解决与弃用的“create_function”函数相关的问题。我使用它来允许我通过一个或多个键名动态地对关联数组进行排序...例如:
usort($myarray, create_function('$a,$b', get_usort_function('field2 ASC, field5 ASC')));

现在,我已经阅读到应该使用匿名函数,所以将代码更改为以下内容...

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

get_usort_function用于创建比较所需的文本 - 因此对于上面的示例,它将返回类似于...

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

现在,在PHP8版本中匿名函数没有起作用 - 但如果我硬编码get_usort_function返回的字符串,它就会起作用。我错过了什么吗?

以下是此操作的简化示例...

<?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 "\n\n***$s***\n\n";

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

?>

上述示例给出了...的输出。
***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
        )

)

我很希望解决这个问题,因为我的网站大量使用了usort函数!所以,显然需要最小的重新工作量是梦想...
提前感谢您, 达伦

1
如果您想动态创建代码,就需要使用 eval() - Barmar
2个回答

1

问题

create_function需要两个字符串参数,分别是要创建的函数的参数和函数体。在内部,它使用eval来创建可调用的内容,这通常被视为不好的做法,因为它增加了攻击面。

从您的描述中可以看出,get_usort_function函数返回一个字符串;正如您所指出的,如果像下面这样调用:

get_usort_function('field2 ASC, field5 ASC')

它将返回类似以下内容的内容:
$field2=compare_ints($a['field2'], $b['field2']); if($field2==0){return compare_ints($a['field5'], $b['field5']);}else{return $field2;}

您已经注意到在传递给 usort 的可调用函数中硬编码字符串是有效的,我想这应该类似于:

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

但更准确的硬编码,根据上述有关 get_usort_function 工作原理的描述,应该是:

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

当像这样书写时,很明显将usort调用从create_function更改为使用可调用对象,就不会返回任何内容(因此usort将保留所有元素的现有顺序)。这可能是您对其工作方式理解不清楚的部分。
解决方案
您可以执行类似以下操作的操作(这可能类似于现有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);

它会产生以下输出:

9: Andy
6: Carol
5: Bob

5: Bob
6: Carol
9: Andy

9: Andy
5: Bob
6: Carol

这里的主要要点是在由get_usort_callable返回的匿名函数上使用use关键字,以便可以使用$fields。如果您想要匹配现有get_usort_function的功能,则可以将其重写为接受string并将其拆分。

最小工作量

考虑到您必须远离create_function以使用PHP 8,您可以做的最少的工作是将get_usort_function重写为返回可调用对象(类似于上面的方法),并替换以下调用:

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

使用:

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

假设您可以访问get_usort_function的内部逻辑,那么这不应该太困难,幸运的是这只在一个位置。调用点的重构也相当机械化,几乎可以在任何IDE中使用查找和替换功能。

从现在开始(根据您的喜好),您可能希望使用结构化数组替换SQL ORDER BY风格的字符串,例如:

'field2 ASC, field5 ASC'

成为:

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

为了避免在get_usort_function中可能出现需要额外处理的空格等问题。

create_function与匿名函数

从使用create_function转换为使用匿名函数时,您不再需要将函数的参数和主体作为字符串传递。例如,以下两个可调用对象是等效的:

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

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

并且可以互换使用。

通过使用以下函数,增加一级间接性(就像你在问题中所做的那样):

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

这两个也是等价的:

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

sort_by_callable('id')

这个主要的结论是,sort_by_callable 函数本身返回一个专门的匿名函数,该函数将根据传入的字段进行排序,而不是包含执行相同逻辑的代码的字符串。

0

谢谢!

我已经采纳了您的建议并重新调整了一些东西,以获得我需要的工作解决方案...

$a1 = array( 'id' => 7, 'name' => 'Andy', 'dob' => '01-02-2020');
$a2 = array( 'id' => 7, 'name' => 'Bob', 'dob' => '01-02-2022');
$a3 = array( 'id' => 9, 'name' => 'Guy', 'dob' => '01-02-2021');
$a4 = array( 'id' => 7, 'name' => 'Mick', 'dob' => '01-02-2023');
$a5 = array( 'id' => 3, 'name' => 'Daz', 'dob' => '01-02-2019');
$a6 = array( 'id' => 7, 'name' => 'Daz', 'dob' => '01-02-2021');

$a = array($a1, $a2, $a3, $a4, $a5, $a6);

function get_usort_function_callable($order_bys, $mode = 1)
{
    return function ($a, $b) use ($order_bys, $mode)
    {
        // We get the options in reverse order, so we can properly nest them.
        $order_by_options_r = array_reverse(explode(",", $order_bys));
        #print_r($order_by_options_r);
        #print "MODE = $mode";

        $first_element = TRUE;
        
        foreach ($order_by_options_r as $order_by)
        {
            if (( isset($retval) ) AND ( strlen($retval) > 0 ))
            {
                $inner_val = $retval;
            }

            $order_by = trim($order_by);
            $indexOfSpace = strpos($order_by, " ");

            if( $indexOfSpace !== FALSE )
            {
                $column = trim(substr($order_by,0,$indexOfSpace));
                $sortorder = trim(substr($order_by,$indexOfSpace));

                if ( strcasecmp($sortorder,"DESC") == 0 )
                {
                    switch ( $mode )
                    {
                        case 1:
                        case 2:
                            $comp = '$b[' . "'" . $column . "'" . '] <=> $a[' . "'" . $column . "'" . ']';
                        break;
                        case 3:
                            $comp = 'strtotime($b[' . "'" . $column . "'" . ']) <=> strtotime($a[' . "'" . $column . "'" . '])';
                        break;
                        case 4:
                            $comp = 'strnatcmp($b[' . "'" . $column . "'" . '], $a[' . "'" . $column . "'" . '])';
                        break;
                    }
                } else {
                    switch ( $mode )
                    {
                        case 1:
                        case 2:
                            $comp = '$a[' . "'" . $column . "'" . '] <=> $b[' . "'" . $column . "'" . ']';
                        break;
                        case 3:
                            $comp = 'strtotime($a[' . "'" . $column . "'" . ']) <=> strtotime($b[' . "'" . $column . "'" . '])';
                        break;
                        case 4:
                            $comp = 'strnatcmp($a[' . "'" . $column . "'" . '], $b[' . "'" . $column . "'" . '])';
                        break;
                    }
                }

                if($first_element)
                {
                    $retval = 'return ' . $comp . ';';
                    $first_element = FALSE;
                } else {
                    $retval = '$' . $column . '=' . $comp . '; if ( $' . $column . ' == 0 ) { ' . $inner_val . ' } else { return $' . $column . '; }';
                }
            }
        }

        #print "RET = $retval\n";

        return eval($retval);
    };
}

usort($a, get_usort_function_callable("dob ASC, id DESC, name DESC") );

print_r($a);

现在这个程序基本上完全符合我的需求,太空船操作符使我能够摆脱额外的“helper”函数(即compare_dates、compare_ints),最终的eval(生成的字符串)意味着返回的是函数而不是函数的字符串。

我不确定是否真的应该使用eval() - 可能不应该,但考虑到函数内容是动态的(即由于字段按动态排序),所以我认为可能是有道理的。

现在我只需要稍微重新编写代码,使所有这些调用......

usort($wishlist_details, create_function('$a,$b', get_usort_function('wishlistcountryname ASC, wishlistname ASC')));

...变成了这样的调用...

usort($_wishlist_details, get_usort_function_callable('wishlistcountryname ASC, wishlistname ASC') );

最好先整理一下我的正则表达式技能,以进行全局搜索/替换,而不是手动编辑每个文件!

谢谢你的帮助 :-)


1
我已经在我的答案中添加了一些更多的信息,以使其更加简洁,但是您当前的 get_usort_function_callable 函数需要更改的主要内容是它不应该返回一个返回要执行的 PHP 代码字符串的函数,而应该返回一个实际上就是那段代码的函数。此外,usort 和类似函数的操作方式是运行可调用函数来比较数组中的两个元素,并将其用于排序,因此对于给定的数组,它将被调用多次。 - msbit
我刚刚更新了我的答案(实际上是回复),提供了最终的可行解决方案。感谢你们的帮助! - darrensunley
一切都好。似乎在重构的过程中已经进行了一些清理,这总是你在修改代码时想要的;让它比你发现时更好一些 :) 你应该考虑在某个时候删除 eval,但这可以留到以后再做。你在这里的使用并不太糟糕,因为它正在评估的字符串仅基于固定输入(即来自用户的任何内容),但你也可能会从一个直接的代码解决方案中找到更好的性能。 - msbit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接