如何避免使用isset()和empty()函数

99

我有几个旧的应用程序,在E_NOTICE错误级别下运行时会抛出许多“xyz未定义”和“未定义的偏移量”消息,因为变量的存在没有使用 isset() 等显式检查。

我正在考虑逐步修改它们,使它们能够兼容 E_NOTICE 错误级别,因为对于缺少变量或偏移量的通知可能是救命稻草,可能还可以获得一些轻微的性能提升,并且这样做更加干净整洁。

然而,我不喜欢给我的代码带来过多的变量检查,如 isset(), empty()array_key_exists() 等,因为这会使我的代码变得臃肿,不易读,而且并没有在价值或含义方面获得任何好处。

在不过多变量检查的情况下,如何构建代码同时又能兼容 E_NOTICE 错误级别?


6
我完全同意。这就是为什么我很喜欢Zend Framework,因为它的请求模块非常好。如果我在做一些小应用程序,我通常会编写一些简单的请求类,其中包括魔术方法__set和__get,其工作方式类似于ZF的请求。这样我就避免了代码中所有关于isset和empty的出现。这样你只需要在迭代数组之前使用if (count($arr) > 0),并且在少数关键位置使用if (null !== $variable)。这看起来更加清洁。 - Richard Knop
11个回答

130

有兴趣的人可以参考我的文章,将以下信息以更好的结构呈现:PHP中isset和empty的权威指南


我认为你不应该只是让应用程序“支持E_NOTICE”,而应该重构整个程序。在代码中有数百个常常试图使用不存在变量的点听起来像是一个非常糟糕结构的程序。尝试访问不存在变量永远都不会发生,其他语言会在编译时反驳这一点。PHP能够让你这么做并不意味着你应该这么做。

这些警告是为了帮助你,而不是为了烦扰你。如果你收到警告"您正在尝试使用不存在的内容!",你的反应应该是"哎呀,我的错误,让我尽快修复它。" 。否则,你怎样才能区分“未定义时运行良好的变量”可能导致严重错误的错误代码?这也是为什么你总是需要,在开发过程中 始终 将错误报告打开到11级 并坚持不懈地修复代码,直到没有任何NOTICE发出。关闭错误报告只用于生产环境,以避免信息泄漏并在存在错误的情况下提供更好的用户体验。


具体来说:

你始终需要在代码中某处使用issetempty,减少它们的出现的唯一方法是正确初始化变量。根据情况有不同的方式来做到这一点:

函数参数:

function foo ($bar, $baz = null) { ... }

不需要在函数内部检查$bar$baz是否已设置,因为您刚刚设置它们,您所需要关心的只是它们的值是否评估为truefalse(或其他任何值)。

可以在任何地方使用常规变量:

$foo = null;
$bar = $baz = 'default value';

将变量初始化放在代码块的顶部,以便在使用它们时解决!isset问题,确保您的变量始终具有已知的默认值,为读者提供以下代码将要处理的内容,并因此作为一种自我文档的形式。

数组:

$defaults = array('foo' => false, 'bar' => true, 'baz' => 'default value');
$values = array_merge($defaults, $incoming_array);
与上面的情况相同,您正在使用默认值初始化数组,并使用实际值覆盖它们。
在其余的情况下,假设您正在输出由控制器设置或未设置的值的模板,您只需检查:
<table>
    <?php if (!empty($foo) && is_array($foo)) : ?>
        <?php foreach ($foo as $bar) : ?>
            <tr>...</tr>
        <?php endforeach; ?>
    <?php else : ?>
        <tr><td>No Foo!</td></tr>
    <?php endif; ?>
</table>

如果你经常使用array_key_exists,你应该评估一下你使用它的目的。它只在以下情况下起作用:

$array = array('key' => null);
isset($array['key']); // false
array_key_exists('key', $array); // true
正如上面所述,如果您正确初始化变量,则不需要检查键是否存在,因为您知道它存在。如果您从外部源获取数组,该值很可能不是null,而是''0'0'false或类似值,即可使用issetempty进行评估,具体取决于您的意图。如果您经常将数组键设置为null并希望它表示除false之外的任何内容,即使在上面的示例中,issetarray_key_exists的不同结果对您的程序逻辑产生影响,您也应该问自己为什么。变量的存在本身并不重要,只有其值才是有意义的。如果键是true/false标志,则应使用truefalse,而不是null。唯一的例外是第三方库想让null表示某些含义,但由于在PHP中很难检测到null,因此我尚未找到任何使用这种方法的库。

4
没错,但大多数访问失败的尝试都是类似于 if ($array["xyz"]) 而不是 isset()array_key_exists(),我认为这种情况有些合理,绝对不是结构性问题(如果我错了,请纠正我)。在我的看法中,添加 array_key_exists() 看起来只是在浪费时间。 - Pekka
9
我想不出任何情况下我会使用array_key_exists而不是简单的isset($array['key'])或者!empty($array['key'])。当然,这两个方法都会在你的代码中增加7到8个字符,但我不会认为这是一个问题。这也有助于澄清你的代码:if (isset($array['key']))表示这个变量确实是可选的,可能不存在;而if ($array['key'])只是表示“如果为真”。如果你得到了后一个方法的提示,你就知道你的逻辑出了问题。 - deceze
6
我认为 isset() 和 array_key_exists() 的区别在于,后者如果值为 NULL,也会返回 true,而 isset() 则不会。 - Htbaa
1
对啊,但我想不出有理智的使用案例,需要区分不存在的变量和值为空的设置键。如果该值评估为FALSE,则区别应该没有任何影响。 :) - deceze
1
@deceze:在数组中区分NULL和其他“假值”有许多合理的用例。其中一个是将关联数组直接映射到关系数据库行(例如MySQL,它区分NULL"")。 - FtDRbwLXw6
显示剩余5条评论

37

只需编写一个函数即可。像这样:

function get_string($array, $index, $default = null) {
    if (isset($array[$index]) && strlen($value = trim($array[$index])) > 0) {
        return get_magic_quotes_gpc() ? stripslashes($value) : $value;
    } else {
        return $default;
    }
}

你可以使用它作为

$username = get_string($_POST, 'username');

对于像get_number()get_boolean()get_array()这样的琐碎事情也要做同样的处理。


5
这看起来不错,而且还进行了magic_quotes检查。很好! - Pekka
很棒的函数!非常感谢分享。 - Mike Moore
3
请注意,$_POST ['something'] 可能返回数组,例如带有<input name="something[]" />的输入。使用上面的代码会导致错误(因为trim不能应用于数组),在这种情况下,应该使用is_string和可能的strval。这不仅仅是一个需要使用get_array的情况,因为用户输入(恶意)可能是任何内容,用户输入解析器无论如何都不应抛出错误。 - Ciantic
1
我使用相同类型的函数,但定义如下: function get_value(&$item, $default = NULL) { return isset($item) ? $item : $default; } 这个函数的优点是你可以用它来处理数组、变量和对象。缺点是如果$item未被初始化,它将被初始化为 null。 - Mat
你应该全局关闭魔术引号,而不是在一个函数中处理它们。互联网上有很多关于魔术引号的资料可以参考。 - Kayla

13

我认为解决这个问题的最佳方法之一是通过访问GET和POST(COOKIE,SESSION等)数组的值来使用一个类。

为每个数组创建一个类,并声明__get__set方法(overloading)。__get接受一个参数,该参数将是一个值的名称。此方法应在相应的全局数组中检查此值,可以使用isset()empty(),如果存在该值,则返回该值,否则返回null(或其他默认值)。

此后,您可以自信地以以下方式访问数组值:$POST->username并进行任何必要的验证,而无需使用任何isset()empty()。如果相应的全局数组中不存在username,则将返回null,因此不会产生任何警告或通知。


1
这是一个很好的想法,我已经准备好重构代码了。+1 - Pekka
不幸的是,除非您将它们分配给 $_GET 或 $_POST,否则您将无法使这些实例成为超全局变量,这将非常丑陋。但是您当然可以使用静态类... - ThiefMaster
1
你不能在“静态类”上使用getter和setter。每个变量编写一个类是不好的实践,因为它意味着代码重复,这是不好的。我认为这个解决方案并不是最合适的。 - Mat
一个类的公共静态成员就像是一个超级全局变量,例如:HTTP::$POST->username,其中你需要在使用之前实例化HTTP::$POST,例如:Class HTTP { public static $POST = array();...}; HTTP::$POST = new someClass($_POST);... - velcrow

6
我不介意使用array_key_exists()函数。事实上,我更喜欢使用这个特定的函数,而不是依赖于hack函数,因为它们在未来可能会改变行为,例如emptyisset(通过删除以避免漏洞)。
然而,在处理数组索引等情况下,我使用一个简单的函数,它在这种情况下非常方便。
function Value($array, $key, $default = false)
{
    if (is_array($array) === true)
    {
        settype($key, 'array');

        foreach ($key as $value)
        {
            if (array_key_exists($value, $array) === false)
            {
                return $default;
            }

            $array = $array[$value];
        }

        return $array;
    }

    return $default;
}

假设你有以下数组:
$arr1 = array
(
    'xyz' => 'value'
);

$arr2 = array
(
    'x' => array
    (
        'y' => array
        (
            'z' => 'value',
        ),
    ),
);

如何从数组中获取“值”?简单:
Value($arr1, 'xyz', 'returns this if the index does not exist');
Value($arr2, array('x', 'y', 'z'), 'returns this if the index does not exist');

我们已经学习了一维数组和多维数组,我们还能做些什么呢?
以以下代码为例:
$url = 'https://dev59.com/OnI-5IYBdhLWcg3wO1rl';
$domain = parse_url($url);

if (is_array($domain) === true)
{
    if (array_key_exists('host', $domain) === true)
    {
        $domain = $domain['host'];
    }

    else
    {
        $domain = 'N/A';
    }
}
else
{
    $domain = 'N/A';
}

这很无聊,还有另一种方法是使用 Value() 函数:

$url = 'https://dev59.com/OnI-5IYBdhLWcg3wO1rl';
$domain = Value(parse_url($url), 'host', 'N/A');

作为另一个例子,可以尝试使用RealIP()函数

$ip = Value($_SERVER, 'HTTP_CLIENT_IP', Value($_SERVER, 'HTTP_X_FORWARDED_FOR', Value($_SERVER, 'REMOTE_ADDR')));

很整洁,是吗?;)

6
"依赖可能在未来更改其行为的黑客函数"?!抱歉,但这是我本周听到的最荒谬的事情之一。首先,issetempty语言结构,而不是函数。其次,如果任何核心库函数/语言结构更改其行为,您可能会陷入麻烦。如果array_key_exists更改其行为怎么办?答案是不会,只要按照文档使用它即可。而且isset的使用方式也已经文档化。最坏情况下,函数会在一个或两个主要版本中被弃用。自己重复造轮子是不好的! - deceze
3
我想说issetemptyarray_key_exists一样可靠,且可以完成同样的工作。你的第二个冗长的例子可以用核心语言特性写成 $domain = isset($domain['host']) ? $domain['host'] : 'N/A';,无需额外的函数调用或声明。(请注意,我不一定主张使用三元操作符; o))。对于普通标量变量,你仍需要使用issetempty,并且在数组中可以以完全相同的方式使用它们。"可靠性"不是不这样做的一个不好的理由。 - deceze
1
你表达了你的观点,虽然我不同意你说的大部分内容。我认为你在90%以上的情况下都弄错了,例如我经常在表单中的隐藏字段中使用“0”的值。尽管如此,我仍然相信我提供的解决方案不应该被贬低,并且可能对Pekka有所帮助。 - Alix Axel
2
虽然@deceze提到了自定义函数的观点,我通常持相同立场,但value()方法看起来很有趣,我会仔细研究一下。我认为这个答案和后续的讨论将使以后遇到类似问题的人能够自行决定。+1。 - Pekka
不错的函数,就我而言,我同意Alix的观点,这是一个非常方便的函数,可以节省很多打字时间,并且在处理我经常处理的多维数组时也可以节省很多时间。+1和谢谢。 - Erik Čerpnjak
显示剩余10条评论

4
欢迎使用 空值合并运算符(PHP >= 7.0.1):
$field = $_GET['field'] ?? null;

PHP说:

空合并运算符(??)作为语法糖被添加,常用于需要在isset()的情况下与三元运算符一起使用。如果第一个操作数存在且不为NULL,则返回第一个操作数;否则返回第二个操作数。


3

我使用这些函数

function load(&$var) { return isset($var) ? $var : null; }
function POST($var) { return isset($_POST[$var]) ? $_POST[$var] : null; }

范例

$y = load($x); // null, no notice

// this attitude is both readable and comfortable
if($login=POST("login") and $pass=POST("pass")) { // really =, not ==
  // executes only if both login and pass were in POST
  // stored in $login and $pass variables
  $authorized = $login=="root" && md5($pass)=="f65b2a087755c68586568531ad8288b4";
}

2
我也使用这个,但要记住,在某些情况下,您的变量将自动初始化:例如,load($array['FOO']) 将在 $array 中创建一个 FOO 键。 - Mat

3

我在这里与你同在。但是PHP设计师犯了比这更多、更严重的错误。除非为任何值读取定义自定义函数,否则没有其他方法。


1
isset()的东西。默认将所有内容设置为null可以避免很多麻烦。 - vava
2
那么这个“everything”是什么呢?PHP似乎要浪费很多资源来想象所有可能的变量名,并将其设置为NULL,只是为了让懒惰的开发人员避免输入五个字符。 - Lotus Notes
5
@Byron,看,这很简单,许多其他语言都可以做到,例如Ruby和Perl。虚拟机知道变量是否被使用过,对吧?它总是可以返回null而不是失败,无论是否带有错误消息。这与糟糕的5个字符无关,而是关于编写params["width"] = params["width"] || 5来设置默认值,而不是使用所有那些关于isset()调用的废话。 - vava
3
抱歉挖起了一个旧的话题。PHP 最糟糕的两个错误是 register_globalsmagic_quotes。由此带来的问题使得未初始化的变量看起来几乎毫不损伤。 - staticsan

1
创建一个函数,如果未设置,则返回false,如果指定了,则如果为空则返回false。如果有效,则返回变量。您可以根据下面的代码添加更多选项:
<?php
function isset_globals($method, $name, $option = "") {
    if (isset($method[$name])) {    // Check if such a variable
        if ($option === "empty" && empty($method[$name])) { return false; } // Check if empty 
        if ($option === "stringLength" && strlen($method[$name])) { return strlen($method[$name]); }    // Check length of string -- used when checking length of textareas
        return ($method[$name]);
    } else { return false; }
}

if (!isset_globals("$_post", "input_name", "empty")) {
    echo "invalid";
} else {
    /* You are safe to access the variable without worrying about errors! */
    echo "you uploaded: " . $_POST["input_name"];
}
?>

0

我不确定你对可读性的定义是什么,但正确使用empty()、isset()和try/throw/catch块对整个过程非常重要。

如果你的E_NOTICE来自于$_GET或$_POST,那么它们应该与所有其他安全检查一起检查是否为空(empty())。

如果它来自于外部源或库,则应该用try/catch包装。

如果它来自于数据库,则应该检查$db_num_rows()或其等效项。

如果它来自于内部变量,则应该正确初始化。通常,这些类型的通知来自将新变量分配给在失败时返回FALSE的函数的返回值。这些应该被包装在一个测试中,在发生故障的情况下,可以将变量分配为代码可以处理的可接受默认值,或者抛出代码可以处理的异常。

这些东西使代码变得更长,增加了额外的块和测试,但我不同意你的看法,我认为它们绝对增加了额外的价值。


0

软件并非神的恩赐,它不会自动运行。如果你期望某些东西却没有出现,你需要妥善处理它。

如果你忽略它,很可能会在你的应用程序中创建安全漏洞。在静态语言中,访问未定义的变量是不可能的。如果它为空,它不会简单地编译或崩溃你的应用程序。

此外,这会使你的应用程序难以维护,当意外事件发生时,你会变得疯狂。语言的严格性是必须的,而 PHP 在许多方面都是错误的设计。如果你没有意识到这一点,它会让你成为一个糟糕的程序员。


我非常清楚PHP的缺陷。正如我在问题中指出的那样,我谈论的是对旧项目的彻底改进。 - Pekka
同意。作为一名长期使用PHP的开发者,尝试进入需要声明所有内容的Java等新语言确实很困难。 - Dzhuneyt

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