Home >Backend Development >PHP Tutorial >Sharing practical knowledge points and pitfalls in PHP learning
This article will share with you some useful knowledge and pitfalls in PHP. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.
When I accidentally checked the PHP documentation, I found some interesting content. As I read more, I found more and more interesting content or time pits. , so I decided to record it and share it. Some of the content below is excerpted from some excellent blogs, user notes of PHP documents, or the original document.
Especially the original text of the document, I found that many people will not read it, and they will not pay attention to many things (yes, I am like this too, so I will take this opportunity to learn it together.)
PHP is a glue that brings together hundreds of external libraries, so sometimes this gets messy. However, a simple rule of thumb is as follows:
Array functionparameters are ordered as " needle, haystack " whereas String functions are the opposite, so " haystack, needle".
Translation: The parameter order of array-related methods is, "needle, haystack", while the parameter order of string-related methods is "haystack, needle",
Source: https://www.php.net/manual/zh/faq.using.php#faq.using.parameterorder
When using the password_hash() or crypt() function, the "salt" will be returned as part of the generated hash value. You can store the complete return value directly in the database, because this return value already contains enough information and can be used directly in the password_verify() or crypt() function for password verification.
The following figure shows the structure of the return value of the crypt() or password_hash() function. As you can see, the algorithm information and the "salt" are already included in the return value, which will be used in subsequent password verification.
Source: https://www.php.net/manual/zh/faq.passwords.php#faq.password.storing-salts
<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."; ?>
In PHP, the end mark of a piece of code is either "?>" or "?>\n" (\n represents a newline). So in the above example, the output sentences will appear on the same line because PHP ignores newlines after the end of code tag. This means that if you want to output a newline character, you need to add an extra newline after the closing tag of each piece of PHP code.
Why does PHP do this? Because this is usually easier when formatting normal HTML. If a line break is output and you don't need this line break, you will have to use a very long line to achieve this effect, or the format of the source file of the generated HTML page will be difficult to read.
Source: https://www.php.net/manual/zh/faq.using.php#faq.using.newlines
If you run the following code, it will output a warning
and result 3
because of the string concatenation operator .
and the mathematical operators
and -
have the same priority, and they will be executed from left to right. Result:
will be forced into an array 0
. If you are running in a lower version of PHP, you will be told that the middle side is not a number. If you are running in 7.4, you will be told that in PHP 8,
and -
take precedence. level will be increased. If you use the EA plug-in in PHPSTORM, you will be reminded of this problem.
<php $var = 3; echo "Result: " . $var + 3;
If you don't want this, then it's better to wrap it in parentheses, like below.
<?php $var = 3; echo "Result: " . ($var + 3);
Source: https://www.php.net/manual/zh/language.operators.string.php#41950
Run the following code, especially the third line. Please note that if there are spaces around .
, even a number will be used as a string connection.
<?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
Source: https://www.php.net/manual/zh/language.operators.string.php#41950
<?php $arr = array('test' => null, 'test2' => 1); // test2=1 echo http_build_query($arr);
Source: https://www.php.net/manual/zh/function.http-build-query.php#60523
<?php $a = [teste1= true,teste2=false]; // teste1=1&teste2=0 echo http_build_query($a)
Source: 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);
in the results Source: https://www.php.net/manual/zh/function.http-build-query.php#109466
PHP will go through the following 4 steps when executing this code (to be precise, it should be PHP's language engine Zend)
现在有的Cache比如APC,可以使得PHP缓存住Opcodes,这样,每次有请求来临的时候,就不需要重复执行前面3步,从而能大幅的提高PHP的执行速度。
来源: https://www.laruence.com/2008/06/18/221.html
<?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<1> 96 "var_dump" L2 #1 SEND_VAL "10.9" 1 L2 #2 DO_ICALL L35 #3 RETURN<-1> 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
这里有一个核心的背景是, 长久一来我们习惯了使用一个名为"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")就变成可被控制的了. 那么如果你的所有类似的请求, 都会被代理到攻击者想要的地址,之后攻击者就可以伪造,监听,篡改你的请求了
所以, 这个漏洞要影响你, 有几个核心前提是:
以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
运行下面的代码,第一个 $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
你是否曾经写过下面这样的代码?
<?php class A { } $A = new A(); var_dump((! $A instanceof A)); // 其实不用担心,因为 instanceof 的优先级要高于 ! ,你可以放心的使用, // 不必添加括号,让他们看起来是一个表达式,但是在复杂的情况下例外。 var_dump(! $A instanceof A);
在你需要对 instanceof
运算的结果做取反运算时,因为取反运算符 !
的优先级低于 instanceof
所以,你不必再它们外面再加上一个圆括号来表明这是一组表达式,但是再复杂情况下例外。
通常,我会使用 array_map
来处理一个数组,让他返回一个新的数组,当然,它的用处就是这样的,但是除了这种基础的用法,它其实还有一些有趣的用法,并且,这些用法都存在于 PHP 的手册中。
通常你会这样使用它。
<?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 代码,如果没有做好处理,被用户利用了, 就有可能会造成安全漏洞,所以最好想办法禁用它,谈到禁用 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
听起来没什么用但是你确实可以这样做。
<?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
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额,比如像下面这些链接。
如果想避免一个方法被外部可见或者子类可见,可以采用 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
递增、递减不能使用在 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
<?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
https://www.php.net/manual/zh/language.operators.array.php
数组还可以相加 (+),用来合并数组,使用 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
推荐学习:《PHP视频教程》
The above is the detailed content of Sharing practical knowledge points and pitfalls in PHP learning. For more information, please follow other related articles on the PHP Chinese website!