まえがき
PHP のドキュメントを偶然見ていたら、興味深いことがいくつか見つかりました。 , 読んでいくうちに、ますます興味深いコンテンツやタイムピットを見つけたので、記録して共有することにしました。以下のコンテンツの一部は、いくつかの優れたブログ、PHP ドキュメントのユーザー ノート、またはドキュメントのオリジナルから抜粋したものです。
特にドキュメントの原文は、多くの人が読まないし、多くのことに注意を払わないことがわかりました(はい、私も同じです。なので、この機会に学ぼうと思います) )
PHP 関数のパラメーターの順序を忘れました。ランダムですか?
PHP は何百もの外部ライブラリをまとめる接着剤であるため、場合によっては面倒になることがあります。ただし、単純な経験則は次のとおりです:
配列関数のパラメータは「針、干し草の山」の順序で並べられます。 "一方、文字列関数はその逆なので、"干し草、針"。
翻訳: 配列関連のメソッドのパラメータの順序は「針、干し草」ですが、文字列関連のメソッドのパラメータの順序は「針、干し草」です。メソッドは「干し草、針」、
出典: https://www.php.net/manual/zh/faq.using.php#faq.using.parameterorder
「塩」はどうやって貯めればいいの?
password_hash() または crypt() 関数を使用すると、生成されたハッシュ値の一部として「salt」が返されます。この戻り値にはすでに十分な情報が含まれており、パスワード検証のために passwd_verify() または crypt() 関数で直接使用できるため、完全な戻り値をデータベースに直接保存できます。
次の図は、crypt() 関数またはpassword_hash() 関数の戻り値の構造を示しています。ご覧のとおり、アルゴリズム情報と「ソルト」は戻り値にすでに含まれており、後続のパスワード検証で使用されます。
出典: https://www.php.net/manual/zh/faq.passwords.php#faq.password.storing-salts
次のコードが 2 つに表示されないのはなぜですか線?
<pre class="brush:php;toolbar:false"> <?php echo "This should be the first line."; ?> <?php echo "This should show up after the new line above."; ?>
PHP では、コードの終了マークは「?>」または「?>\n」のいずれかです (\n は改行を表します)。したがって、上記の例では、PHP はコード終了タグの後の改行を無視するため、出力文は同じ行に表示されます。つまり、改行文字を出力したい場合は、PHP コードの各部分の終了タグの後に追加の改行を追加する必要があります。
PHP がこのようなことを行うのはなぜですか?これは、通常の HTML をフォーマットする場合の方が簡単だからです。改行が出力されるが、この改行が必要ない場合、この効果を実現するには非常に長い行を使用する必要があり、そうしないと、生成される HTML ページのソース ファイルの形式が読みにくくなります。
出典: https://www.php.net/manual/zh/faq.using.php#faq.using.newlines
文字列連結演算子の優先事項
次のコードを実行すると、文字列連結演算子 .
と数学演算子 ## が原因で、
警告 と結果
3 が出力されます。 # と
- は同じ優先度を持ち、左から右に実行されます。
Result: は配列
0 に強制的に挿入されます。以前のバージョンの PHP で実行している場合は、真ん中の辺は数字ではないというメッセージが表示され、7.4 で実行している場合は、PHP 8 では、
と
というメッセージが表示されます。 - が優先され、レベルが上がります。 PHPSTORM で EA プラグインを使用すると、この問題が発生することがわかります。
<php echo>これを望まない場合は、以下のように括弧で囲むことをお勧めします。 <p></p><pre class="brush:php;toolbar:false"><?php $var = 3; echo "Result: " . ($var + 3);出典: https://www.php.net/manual/zh/ language.operators.string.php#41950文字列連結演算子と数値Run次のコード、特に 3 行目
. の周囲にスペースがある場合、数値であっても文字列接続として使用されることに注意してください。
<?php echo "thr"."ee"; //prints the string "three" echo "twe" . "lve"; //prints the string "twelve" echo 1 . 2; //prints the string "12" echo 1.2; //prints the number 1.2 echo 1+2; //prints the number 3出典: https://www.php.net/manual/zh/ language.operators.string.php#41950http_build_queryNULL の値を使用しますto 無視されます
<?php $arr = array('test' => null, 'test2' => 1);
// test2=1
echo http_build_query($arr);
出典: https://www.php.net/manual/zh/function.http-build-query.php#60523True と False は無視されます数値に変換<?php $a = [teste1= true,teste2=false];
// teste1=1&teste2=0
echo http_build_query($a)
出典: https://www.php.net/manual/zh/function.http-build-query.php#122232空の配列は # に表示されません結果の ##<?php $post_data = array('name'=>'miller', 'address'=>array('address_lines'=>array()), 'age'=>23); // name=miller&age=23 echo http_build_query($post_data);
出典: https://www.php.net/manual/zh/function.http-build-query.php#109466
OpCache の原理を簡単に説明します
このコードを実行すると、PHP は次の 4 つのステップを実行します (正確には、PHP の言語エンジン Zend のはずです)
- 1. Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)
- 2. Parsing, 将Tokens转换成简单而有意义的表达式
- 3. Compilation, 将表达式编译成Opocdes
- 4. Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。
现在有的Cache比如APC,可以使得PHP缓存住Opcodes,这样,每次有请求来临的时候,就不需要重复执行前面3步,从而能大幅的提高PHP的执行速度。
来源: https://www.laruence.com/2008/06/18/221.html
var_dump(1...9)输出什么?
<?php // 10.9 var_dump(1...9);
输出10.9, 乍一看这个var_dump的输出很奇怪是不是? 为什么呢?
这里教大家,如果看到一段PHP代码感觉输出很奇怪,第一反应是看下这段代码生成的opcodes是啥,虽然这个问题其实是词法分析阶段的问题,不过还是用phpdbg分析下吧(一般为了防止opcache的影响,会传递-n):
phpdbg -n -p /tmp/1.php function name: (null) L1-35 {main}() /tmp/1.php - 0x7f56d1a63460 + 4 ops L2 #0 INIT_FCALL 96 "var_dump" L2 #1 SEND_VAL "10.9" 1 L2 #2 DO_ICALL L35 #3 RETURN 1
所以这么看来,早在生成opcode之前,1...9就变成了常量10.9,考虑到这是字面量,我们现在去看看zend_language_scanner.l, 找到这么一行:
DNUM ({LNUM}?"."{LNUM})|({LNUM}"."{LNUM}?)
这个是词法分析定义的浮点数的格式,到这里也就恍然大悟了:
1...9 会被依次接受为: 1. (浮点数1), 然后是 . (字符串连接符号) 然后是.9(浮点数0.9)
所以在编译阶段就会直接生成 “1” . “0.9” -> 字符串的字面量”10.9”
来源: https://www.laruence.com/2020/02/23/1990.html
HTTPOXY 漏洞
这里有一个核心的背景是, 长久一来我们习惯了使用一个名为"http_proxy"的环境变量来设置我们的请求代理。
http_proxy=127.0.0.1:9999 wget http://www.laruence.com/
如何形成?
在CGI(RFC 3875)的模式的时候, 会把请求中的Header, 加上HTTP_ 前缀, 注册为环境变量, 所以如果你在Header中发送一个Proxy:xxxxxx, 那么 PHP 就会把他注册为HTTP_PROXY环境变量, 于是getenv("HTTP_PROXY")就变成可被控制的了. 那么如果你的所有类似的请求, 都会被代理到攻击者想要的地址,之后攻击者就可以伪造,监听,篡改你的请求了
如何影响?
所以, 这个漏洞要影响你, 有几个核心前提是:
- 你的服务会对外请求资源
- 你的服务使用了HTTP_PROXY(大写的)环境变量来代理你的请求(可能是你自己写,或是使用一些有缺陷的类库)
- 你的服务跑在PHP的CGI模式下(cgi, php-fpm)
如何处理?
以Nginx为例, 在配置中加入:
fastcgi_param HTTP_PROXY "";
所以建议, 即使你不受此次漏洞影响, 也应该加入这个配置.
而如果你是一个类库的作者,或者你因为什么原因没有办法修改服务配置, 那么你就需要在代码中加入对sapi的判断, 除非是cli模式, 否则永远不要相信http_proxy环境变量,
<?php if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) { //只有CLI模式下, HTTP_PROXY环境变量才是可控的 }
补充: 从PHP5.5.38开始, getenv增加了第二个参数, local_only = false, 如果这个参数为true, 则只会从系统本地的环境变量表中获取, 从而修复这个问题, 并且默认的PHP将拦截HTTP_PROXY:fix
HTTPOXY漏洞说明 - 风雪之隅
https://www.laruence.com/2016/07/19/3101.html
运算符优先级
&& 和 and 在赋值运算中的问题
运行下面的代码,第一个 $bool
将打印为 false
,预期如此,但是第二个 $bool
将打印 true
。这是因为 =
的优先级高于 and
运算符,所以,第二个 $bool
将会被当成 ($bool = true) and false
执行。
<?php $bool = true && false; // false var_dump($bool); $bool = true and false; // true var_dump($bool);
来源: https://www.php.net/manual/zh/language.operators.precedence.php#117390
instanceof 运算符
你是否曾经写过下面这样的代码?
<?php class A { } $A = new A(); var_dump((! $A instanceof A)); // 其实不用担心,因为 instanceof 的优先级要高于 ! ,你可以放心的使用, // 不必添加括号,让他们看起来是一个表达式,但是在复杂的情况下例外。 var_dump(! $A instanceof A);
在你需要对 instanceof
运算的结果做取反运算时,因为取反运算符 !
的优先级低于 instanceof
所以,你不必再它们外面再加上一个圆括号来表明这是一组表达式,但是再复杂情况下例外。
array_map 的有趣用法
通常,我会使用 array_map
来处理一个数组,让他返回一个新的数组,当然,它的用处就是这样的,但是除了这种基础的用法,它其实还有一些有趣的用法,并且,这些用法都存在于 PHP 的手册中。
多个 array 用法
通常你会这样使用它。
<?php $arr1 = ['apple', 'orange', 'mango', 'pear']; $newArr1 = array_map('strtoupper',$arr1);
这只是一个简单的,它会把所有的值转为大写的。那么看看下面的用法,猜猜会打印什么?
<?php $arr1 = ['apple', 'orange', 'mango', 'pear']; $arr2 = ['cauliflower', 'lettuce', 'kale', 'romaine']; function map1($a, $b){ var_dump($a, $b); // apple cauliflower // orange lettuce // mango kale // pear romaine } array_map('map1', $arr1, $arr2);
如上 map1
方法所示,将会顺序遍历 $arr1
, $arr2
中的值,并且传递给 map1
,根据手册所定义: 如果多个数组的长度不一,即短的数组将会被填充空,至长的数组一样 。
原生函数使用不当的话会比你想象的要慢
array_unique、array_merge 等,如果使用方法不正确,会比你想想的要慢,甚至是慢很多,远不如 foreach。
在下面这个回答中,列举了 PHP 中一些 array_* 方法的时间复杂度
performance - List of Big-O for PHP functions - Stack Overflow
小心代码中的比较
下面的比较将会返回 true
,是不是不敢相信?
因为两个 md5 值都有开始'0e',所以PHP类型理解这些字符串是科学符号。根据定义,0 的任何次方都是 0,所以在这里会成立,所以当你确定一个变量的类型时,你最好使用 ===
(恒等于)进行比较。
<?php $a = md5('240610708');// 0e462097431906509019562988736854 $b = md5('QNKCDZO'); // 0e830400451993494058024219903391 var_dump($a == $b); // true
注意,当你在考虑使用 md5 存储密码时,你应该放弃这个想法,应该改用为 password_hash 系列方法。
来自:https://www.php.net/manual/zh/function.md5.php#123392
禁用 PHP 中不安全的 eval 方法
众所周知, 在 php 中,eval 方法可以执行任意 PHP 代码,如果没有做好处理,被用户利用了, 就有可能会造成安全漏洞,所以最好想办法禁用它,谈到禁用 php 函数,你应该想到了 php.ini 中的 disable_functions
参数,可以用来禁用 PHP 函数,一些集成环境中也会禁用一些高风险函数来降低风险。
但是,这个配置项,却禁用不了 eval 函数,因为根据官方文档的定义, eval 不是一个函数,他如同 echo 、这些特殊方法一样,他是一个语法结构,所以不能使用 disable_functions
进行禁用,除此之外,还有 require、list、array、print、unset、include、isset、empty 、die、exit 等,这些都是语法结构,不是函数,如果你使用 function_exists
判断,他们都会返回 false
如果你真的需要禁用 eval ,你得安装一些第三方扩展来实现,比如 mk-j/PHP_diseval_extension
参考:https://www.php.net/manual/zh/functions.variable-functions.php#functions.variable-functions
将任意类型转换为 null
听起来没什么用但是你确实可以这样做。
<?php $a = 'Hi!'; // 在 PHP 7.2 以下,这行代码会返回 null,7.2 ~ 7.4 会返回 NULL,但是会提示被遗弃, // 8.0 开始,将不再支持 var_dump((unset)$a); var_dump($a);
除此之外,你还可以用 settype 函数
参考:https://www.php.net/settype
参考:https://www.php.net/manual/zh/function.unset.php#example-5601
isset 和 unset 同时支持多个参数
unset 支持多个参数,想必大多数人是知道的,但是 isset 也支持哟。
<?php var_dump(isset($a, $b, $c)); unset($a, $b, $c);
你不需要担心这几个变量没有被设置,他们在这里都是安全的,不会报错,在 isset 多个变量时,必须要所有变量都不为 null
时,才会返回 true
,当遇到一个不存在时,将会立即返回。
参考:https://www.php.net/isset
快速查询一个函数或者类或语法的参考
当你要查询一个 php 方法或者对象或者语法时,你不需要打开 php 手册进行搜索,你只需要在 https://php.net/<keyword></keyword>
后面跟上方法、语法、对象的名字即可,并且不需要关心大小i额,比如像下面这些链接。
- https://php.net/curlfile
- https://php.net/isset
- https://php.net/if
使用反射调用 protected 或者 private 的类方法
如果想避免一个方法被外部可见或者子类可见,可以采用 protected 或者 private 关键字来修改这些类,但是我们有时候又想在外部调用这些方法,应该怎么办呢?只能改成 public 吗?如果这是我们自己的代码,当然可以这样做,但是如果是引入的外部代码的话,可能就不太好直接修改了。
现在,我们可以在外部使用 反射 来调用这些方法,现在我们来定义一个 Lisa 类
<?php class Lisa { public function name() { return 'Lisa'; } protected function age() { return 22; } private function weight() { return 95; } private static function eat(){ return 1; } }
通常情况下,我们是没有办法直接调用 age 和 weight 方法的,现在,我们使用反射来调用。
<?php // ... $reflectionClass = new ReflectionClass('Lisa'); $ageMethod = $reflectionClass->getMethod('age'); // 获取 age 方法 $ageMethod->setAccessible(true); // 设置可见性 // 调用这个方法,需要传入对象作为上下文 $age = $ageMethod->invoke($reflectionClass->newInstance()); var_dump($age);// 22
上面的代码看起来有些繁琐,还有一个更简单的办法。
<?php // ... $reflectionClass = new ReflectionClass('Lisa'); $weightMethod = $reflectionClass->getMethod('weight');// 获取 weight 方法 // 获取一个闭包,然后调用,同样需要传入对象作为上下文,后面调用的地方就可以传入参数 $weight = $weightMethod->getClosure($reflectionClass->newInstance())(); var_dump($weight);
调用静态方法
<?php // ... $reflectionClass = new ReflectionClass('Lisa'); $eatMethod = $reflectionClass->getMethod('eat'); $eatMethod->setAccessible(true); $eat = $eatMethod->invoke(null); // 如果是一个静态方法,你可以传递一个 null var_dump($eat);
同样,类成员也可以使用反射进行修改。
参考: https://www.php.net/manual/zh/class.reflectionproperty.php
实例化一个类,但是绕过他的构造方法
有没有这样想过?实例化一个类,但是却不想调用他的构造方法(__construct),这里也可以用反射做到。
<?php class Dog { private $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } } $dogClass = new ReflectionClass('Dog'); // 创建一个新实例,但是不调用构造方法 $dogInstance = $dogClass->newInstanceWithoutConstructor(); var_dump($dogInstance->getName()); // null
如果你的环境不能使用反射,你还可以试试另一个很 cool 的方法,就是使用反序列化,可以参考包 doctrine/instantiator - Packagist
参考: https://www.php.net/manual/zh/reflectionclass.newinstancewithoutconstructor.php
获取类一个类的所有父类
使用 class_parents
可以获取一个类的所有父类,并且支持自动加载。
<?php class A{} class B extends A{} class C extends B{} class D extends C{} var_dump(class_parents('D')); /* array(3) { 'C' => string(1) "C" 'B' => string(1) "B" 'A' => string(1) "A" } */
参考:https://www.php.net/manual/zh/function.class-parents.php
有趣的递增和递减
递增递减不能作用域 bool 值
递增、递减不能使用在 false 上面,但是 +=
和 -=
可以
<?php $a = false; ++$a; var_dump($a);// false $a++; var_dump($a);// false --$a; var_dump($a);// false $a--; var_dump($a);// false $a-= 1; var_dump($a);// -1 $a+= 1;// 因为前面改变了,变成了 -1,所以下面是 0 ,请不要在这里疑惑 var_dump($a);// 0
递增可以作用域 NULL,但是递减不可以
<?php $a = null; ++$a; var_dump($a); //1 $a = null; --$a; var_dump($a); // null
递增可以作用于字母,但是递减不可以
a-y 递增时字母都将向后增加一个,但是当 z 的时候,就将会回到 aa ,循环如此,但是只能递增,不能递减
<?php $a = 'a'; ++$a; var_dump($a); // b $a = 'z'; ++$a; var_dump($a); // aa $a = 'A'; ++$a; var_dump($a); // B $a = 'Z'; ++$a; var_dump($a); // AA
混合递增数字和字母
现在你还可以把字母和数字混合起来,就像这样:
>>> $a = 'A1' => "A1" >>> ++$a => "A2" >>> ++$a => "A3" >>> $a = '001A' => "001A" >>> ++$a => "001B" >>> ++$a => "001C" >>> $a = 'A001' => "A001" >>> ++$a => "A002" >>> ++$a => "A003"
但是请注意一些意外情况,比如这样。
>>> $a = '9E0' => "9E0" >>> ++$a => 10.0
这是因为9E0 被当作成了浮点数的字符串表示,被 PHP 当成了 9*10^0 ,被评估成了 9 ,然后在执行的递增。
参考来源: https://www.php.net/manual/zh...
请注意你的嵌套强制类型转换,否则他会发生意外
<?php var_dump(TRUE === (boolean) (array) (int) FALSE);// true var_dump((array) (int) FALSE);
因为当把 FALSE 转为数字是,他是 0,再转为数组后,就成了,[0]
,所以再转为 boolean 时,将会返回 true,因为数组不为空,并且 [0] != []
参考:https://www.php.net/manual/zh/language.types.type-juggling.php#115373
高版本中的数字与字符串进行比较
自 PHP 8.0 开始。
数字与非数字形式的字符串之间的非严格比较现在将首先将数字转为字符串,然后比较这两个字符串。 数字与数字形式的字符串之间的比较仍然像之前那样进行。 请注意,这意味着 0 == "not-a-number" 现在将被认为是 false 。
Comparison | Before | After |
---|---|---|
0 == "0" | true | true |
0 == "0.0" | true | true |
0 == "foo" | true | false |
0 == "" | true | false |
42 == " 42" | true | true |
42 == "42foo" | true | false |
参考:https://www.php.net/manual/zh/migration80.incompatible.php#migration80.incompatible.core
数组也可以直接比较
你可以直接使用 ==
比较两个数组有相同的键值对,如果这不是一个关联数组,那么就要保证值的顺序相对应,如果时一个关联数组,你就可以不用担心。
>>> $b = [1,2,3,4] => [ 1, 2, 3, 4, ] >>> $a = [1,2,3,4] => [ 1, 2, 3, 4, ] >>> $a == $b => true // 注意,他不会比较类型。 >>> $a = [0,1,2,3,4] => [ 0, 1, 2, 3, 4, ] >>> $b = [false,1,2,3,4] => [ false, 1, 2, 3, 4, ] >>> $a == $b => true // 如果你要比较类型,你应该使用 === >>> $a === $b => false
无序的比较:
下面的列表中,使用 == 将会返回 true ,因为他们的值是相等的,只是顺序不同,但是如果使用 === 将会返回类型,因为 === 的时候会考虑键值顺序和数据类型。
>>> $a = ['name'=>'Jack','sex'=>1,'age'=>18]; => [ "name" => "Jack", "sex" => 1, "age" => 18, ] >>> $b = ['name'=>'Jack','age'=>18,'sex'=>1]; => [ "name" => "Jack", "age" => 18, "sex" => 1, ] >>> $a == $b => true >>> $a === $b => false >>>
来源:PHP: 数组运算符 - Manual
合并数组
数组还可以相加 (+),用来合并数组,使用 array_merge 可以合并数组可以把两个数组相加,想必是都知道的,但是其实 + 号也可以,虽然都是合并数组,这两个方法各有区别。+
更像是替换。
1、使用 array_merge 合并非关联数组时,不会过滤重复项目, + 会(更像是替换)
>>> $a = [1,2,3] => [ 1, 2, 3, ] >>> $b = [2,3,4] => [ 2, 3, 4, ] >>> array_merge($a,$b) => [ 1, 2, 3, 2, 3, 4, ] >>> $a + $b => [ 1, 2, 3, ]
2、使用 array_merge 合并关联数组时,如果键重复,将会保留最后一个数组的值,而使用 + 将会保留第一个键下面的值。
>>> $a = ['name'=>'Jack','sex'=>1,'age'=>18]; => [ "name" => "Jack", "sex" => 1, "age" => 18, ] >>> $b = ['name'=>'Jack','age'=>'18','sex'=>'1']; => [ "name" => "Jack", "age" => "18", "sex" => "1", ] >>> array_merge($a, $b) => [ "name" => "Jack", "sex" => "1", "age" => "18", ] >>> $a + $b => [ "name" => "Jack", "sex" => 1, "age" => 18, ]
3、当关联数组中存在数字键时, array_merge 会重置数字键, + 则不会
>>> $a = ['name'=>'Jack','sex'=>1,'age'=>18]; => [ "name" => "Jack", "sex" => 1, "age" => 18, ] >>> $b = ['name'=>'Jack','age'=>'18','sex'=>'1','10'=>'hi']; => [ "name" => "Jack", "age" => "18", "sex" => "1", 10 => "hi", ] >>> array_merge($a,$b) => [ "name" => "Jack", "sex" => "1", "age" => "18", //注意这里 0 => "hi", ] >>> $a + $b => [ "name" => "Jack", "sex" => 1, "age" => 18, //注意这里 10 => "hi", ]
下面用一张图来概括一下。
图片来源:array_merge vs array_replace vs + (plus aka union) in PHP | SOFTonSOFA
结束
- 文章中大部分内容来自网络搜集,我已经尽所能去验证其真实性,但可能部分会有纰漏,如果有请不吝赐教。
- 另外,如果文中的内容侵犯到了你得权益,请与我联系处理。
- 你还可以点击文章中的来源链接,了解更详细的内容。