分享一些 PHP 中有用的知識和坑

前言

在一次偶然查看 PHP 文檔的時候,發現了一些有趣的內容,隨着閱讀的增長,愈加以爲有趣的內容或者說時坑愈來愈多,因此我決定記錄下來,分享出去,下文中一些內容摘錄自一些優秀的博客、PHP 文檔的用戶筆記,或者文檔原文。php

尤爲是文檔原文,我發現不少人不會去讀,不少東西也不會去注意(是的,我也是這樣,因此藉着此次機會,一塊兒來學習一下。)html

我忘了PHP函數的參數順序,它們是隨機的嗎?

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:git

Array functionparameters are ordered as " needle, haystack" whereas String functionsare the opposite, so " haystack, needle".github

譯:數組相關方法的參數順序爲,「needle, haystack」,字符串相關方法則是相反的 「haystack, needle」,算法

來源: https://www.php.net/manual/zh/faq.using.php#faq.using.parameterorder數據庫

我應該如何保存「鹽」?

當使用 password_hash() 或者 crypt() 函數時, 「鹽」會被做爲生成的散列值的一部分返回。 你能夠直接把完整的返回值存儲到數據庫中, 由於這個返回值中已經包含了足夠的信息, 能夠直接用在 password_verify() 或 crypt() 函數來進行密碼驗證。api

下圖展現了 crypt() 或 password_hash() 函數返回值的結構。 如你所見,算法的信息以及「鹽」都已經包含在返回值中, 在後續的密碼驗證中將會用到這些信息。 數組

來源: https://www.php.net/manual/zh/faq.passwords.php#faq.password.storing-salts緩存

下面代碼怎麼沒有分紅兩行顯示?

<pre>
<?php echo "This should be the first line."; ?>
<?php echo "This should show up after the new line above."; ?>
</pre>

在 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
$var = 3;

echo "Result: " . $var + 3;

若是你不但願這樣,那麼最好使用括號把它包裹起來,就像下面那樣。

<?php
$var = 3;

echo "Result: " . ($var + 3);

來源: https://www.php.net/manual/zh/language.operators.string.php#41950

字符串鏈接操做符與數字

運行下面代碼,尤爲是第三行,請注意,若是 . 左右存在空格,那麼即便是一個數字,也將會做用成字符串鏈接。

<?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#41950

使用 http_build_query

NULL 的值將會被會略

<?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#60523

True 和 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<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

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後面跟上方法、語法、對象的名字便可,而且不須要關心大小i額,好比像下面這些連接。

使用反射調用 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 能夠合併數組能夠把兩個數組相加,相比是都知道的,可是其實 + 號也能夠,雖然都是合併數組,這兩個方法各有區別。+ 更像是替換。

一、使用 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,
   ]

二、使用 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,
   ]

三、當關聯數組中存在數字鍵時, 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",
   ]

下面用一張圖來歸納一下。

Screenshot from 2015-11-13 00:34:21

圖片來源:array_merge vs array_replace vs + (plus aka union) in PHP | SOFTonSOFA

結束

  • 文章中大部份內容來自網絡蒐集,我已經盡所能去驗證其真實性,但可能部分會有紕漏,若是有請不吝賜教。
  • 另外,若是文中的內容侵犯到了你得權益,請與我聯繫處理。
  • 你還能夠點擊文章中的來源連接,瞭解更詳細的內容。

參考

相關文章
相關標籤/搜索