【问题标题】:Anonymous function content in PHP not workingPHP中的匿名函数内容不起作用
【发布时间】:2021-04-05 21:54:45
【问题描述】:

我有一个网站在相当旧的 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 功能!所以,很明显,需要最少的返工是梦想......

提前致谢, 达伦

【问题讨论】:

  • 如果要动态创建代码,需要使用eval()

标签: php


【解决方案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 vs 匿名函数

从使用 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 函数本身返回一个专门的匿名函数,该函数将对传入的字段进行排序,而不是包含执行相同逻辑的代码的字符串。

【讨论】:

    【解决方案2】:

    谢谢!

    我已经采纳了你所说的内容并重新设计了一些东西以获得我需要的有效解决方案......

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

    这几乎完全符合我现在的需要,飞船操作员允许我摆脱额外的“帮助”函数(即 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') );
    

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

    感谢您的帮助:-)

    【讨论】:

    • 我在答案中添加了更多信息,以使其更简洁,但在您当前的 get_usort_function_callable 函数中需要更改的主要内容是它不应该返回函数如果返回一个要执行的 PHP 代码字符串,它应该返回一个 is 该代码的函数,因为没有更好的术语。另外,usort 和类似函数的操作方式是运行 callable 来比较数组中的两个元素,并使用它进行排序,因此对于给定的数组,它将被多次调用。
    • 我刚刚用最终的工作解决方案更新了我的答案(这确实是一个回复)。感谢您的所有帮助!
    • 一切都好。作为重构的一部分,它似乎已经被清理了一点,当你接触代码时,这总是你想要的;让它比你找到它时更好一点:) 你应该在某个时候删除eval,但这可能是以后的工作。您在这里的使用并不算太糟糕,因为它正在评估的字符串仅基于固定输入(即来自用户的任何内容),但您也可以从直接代码解决方案中找到更好的性能。
    猜你喜欢
    • 1970-01-01
    • 2014-08-09
    • 1970-01-01
    • 2022-11-05
    • 1970-01-01
    • 2014-09-11
    • 1970-01-01
    • 2017-07-03
    • 1970-01-01
    相关资源
    最近更新 更多