PHP7.2 新特性 新功能

PHP 7.2 在 2017年十一月30號釋出了

趕緊用 phpbrew 裝在 Mac 上測試玩玩

來看看有哪些新功能吧

New objec type

這個新特性加入了 Object 的 type hitting

現在可以指定傳入的參數為 object (stdClass)

也可以指定回傳的參數是 object

直接看執行結果的差異摟

我用 7.1 跟 7.2 做比較

 $ php -v                                                          
PHP 7.2.0 (cli) (built: Dec 30 2017 22:48:12) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2017 Zend Technologies

 $ php -a                                                          
Interactive shell

php > function test(object $obj) : object
php > {
php {     var_dump($obj);
php {     return $obj;
php { }
php > test(new StdClass());

object(stdClass)#1 (0) {}

 $ php -v                                                          
PHP 7.1.3 (cli) (built: Sep  4 2017 23:40:15) ( NTS DEBUG )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.3, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

 $ php -a                                                          [22:57:45]
Interactive shell

php > function test(object $obj) : object
php > {
php {     var_dump($obj);
php {     return $obj;
php { }
php > test(new StdClass());
PHP Warning:  Uncaught TypeError: Argument 1 passed to test() must be an instance of object, instance of stdClass given, called in php shell code on line 1 and defined in php shell code:1
Stack trace:
#0 php shell code(1): test(Object(stdClass))
#1 {main}
  thrown in php shell code on line 1

Warning: Uncaught TypeError: Argument 1 passed to test() must be an instance of object, instance of stdClass given, called in php shell code on line 1 and defined in php shell code:1
Stack trace:
#0 php shell code(1): test(Object(stdClass))
#1 {main}
  thrown in php shell code on line 1

在 php 7.1 執行一樣的程式碼

會出現 TypeError

無法支援 object 的 type hitting

Extension loading by name

7.2 版本在 ini 設定 load extension 不再需要副檔名

若是 Linux like 系統

7.2 之前要啟用 extension 都需要在 php.ini or optional.ini 內加入

zend_extension=opcache.so

或是在 windows 內要這樣寫

zend_extension=opcache.dll

在使用自動化部屬的時候就無法共用同一份 config

而現在 7.2 只需要 extension 名稱即可自動載入

zend_extension=opcache

一樣來測試看看

 $ cat /Users/herb/.phpbrew/php/php-7.2.0/var/db/opcache.ini        
zend_extension=opcache

 $ php -v                                                           
PHP 7.2.0 (cli) (built: Dec 30 2017 22:48:12) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.2.0, Copyright (c) 1999-2017, by Zend Technologies

7.2版可正常執行

在 7.1版則會出現找不到 extension 的錯誤訊息

cat /Users/herb/.phpbrew/php/php-7.1.3/var/db/opcache.ini        [0:01:55]
zend_extension=/Users/herb/.phpbrew/php/php-7.1.3/lib/php/extensions/debug-non-zts-20160303/opcache

 $ php -v                                                           
Failed loading /Users/herb/.phpbrew/php/php-7.1.3/lib/php/extensions/debug-non-zts-20160303/opcache:  dlopen(/Users/herb/.phpbrew/php/php-7.1.3/lib/php/extensions/debug-non-zts-20160303/opcache, 9): image not found
PHP 7.1.3 (cli) (built: Sep  4 2017 23:40:15) ( NTS DEBUG )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

Abstract method overriding

7.2 版可以複寫 abstract class 的方法

 $ php -a                                                           
Interactive shell

php > abstract class A
php > {
php {     abstract function test(string $s);
php { }
php > abstract class B extends A
php > {
php {     // overridden - still maintaining contravariance for parameters and covariance for return
php {     abstract function test($s) : int;
php { }
php >

7.1 版則會出現錯誤

 $ php -a                                                           
Interactive shell

php > abstract class A
php > {
php {     abstract function test(string $s);
php { }
php > abstract class B extends A
php > {
php {     // overridden - still maintaining contravariance for parameters and covariance for return
php {     abstract function test($s) : int;
php { }
PHP Fatal error:  Can't inherit abstract function A::test() (previously declared abstract in B) in php shell code on line 5

Fatal error: Can't inherit abstract function A::test() (previously declared abstract in B) in php shell code on line 5

php >

Sodium 從PECL移到主要 extension

主要原因為原本 PHP 的加解密函數 mcrypt_* 已經在 7.2 版被移除(7.1只有加上 Deprecate notice)

根據這篇的描述,mcrypt 是一個阻礙語言發展的絆腳石XD

而且 mcrypt 可做到的”所有事”

openssl 系列方法都可以做到

而 Sodium 則比 openssl 效率高

但要裝不裝都可以吧

Password hashing with Argon2

在 php 裡有個 password_hash 的 function

可以指定多種演算法將字串做 hash

7.2 版則新加入一種叫 Argon2 的演算法

搜尋後發現

這是目前最適合做 password hashing的演算法

根據 OWASP 的介紹

Argon2 is the winner of the password hashing competition and should be considered as your first choice for new applications;

Argon2 應當成為下個專案做密碼雜湊的首選演算法

第二才是PBKDF2

而 7.2 之前最熱門的 bcrypt 僅排最後

既然如此就來試用一下吧

 $ php -a                                                          
Interactive shell

php > echo password_hash('my_password', PASSWORD_ARGON2I);
PHP Warning:  Use of undefined constant PASSWORD_ARGON2I - assumed 'PASSWORD_ARGON2I' (this will throw an Error in a future version of PHP) in php shell code on line 1

Warning: Use of undefined constant PASSWORD_ARGON2I - assumed 'PASSWORD_ARGON2I' (this will throw an Error in a future version of PHP) in php shell code on line 1
PHP Warning:  password_hash() expects parameter 2 to be integer, string given in php shell code on line 1

Warning: password_hash() expects parameter 2 to be integer, string given in php shell code on line 1
php >

竟然出現錯誤?

說找不到 PASSWORD_ARGON2I 常數

不是說 7.2 開始支援嗎?

結果發現是在編譯 php 的時候必須要加上 –with-password-argon2 的參數

而且在編譯的環境裡要先裝有 argon2 的 lib and header files

所以我先用 brew 裝 argon2 起來

再重新編譯 php 7.2

$ brew update
$ brew install argon2
$ phpbrew install 7.2.0 +default +mb +mysql +pdo -- --with-password-argon2

重新執行看看

 $ php -a                                                          
Interactive shell

php > echo password_hash('my_password', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$bUxhSldzZGxvUy5RbTF2cw$hfUVzeiIiMCnoMy/+7G/yT9l2fl2PJcdCFfXDOIvDVU

成功摟!

在編譯的時候看到

Argon2 可以防禦 time-memory trade off attack

也就是俗稱的 Rainbow table

所以再次執行 password_hash 出來的結果會不同

 $ php -a                                                          
Interactive shell

php > echo password_hash('my_password', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$bUxhSldzZGxvUy5RbTF2cw$hfUVzeiIiMCnoMy/+7G/yT9l2fl2PJcdCFfXDOIvDVU
php > echo password_hash('my_password', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$UUk1Y1EweDVyb1hZeWUxRA$IW7t2YGeMnIhffEeSDPWzPgEqIGnfQ3BMNk5fyW1ZeQ
php > echo password_hash('my_password', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$UzlvdnJhWjBKakdmZ3BqWQ$QuU4DJ5qI564QhnZgobhmFwGpHFSzHGaMwgHP5G+TSY
php > echo password_hash('my_password', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$aTB5U0ZLdWdXYkN2aEVQUQ$oaM8lJh4j8pwqr/CR+KjNn5siFAiKie09XX7KFOOKR8
php >

果然是最適合做密碼雜湊的演算法!

Extended string types for PDO

7.2 版的 PDO 支援 National character type

這我還不是很明白跟一般的 UTF-8 character 有何差別

可能要再做實驗才能了解

新增的常數如下

  • PDO::PARAM_STR_NATL
  • PDO::PARAM_STR_CHAR
  • PDO::ATTR_DEFAULT_STR_PARAM

這要跟一般的 PARAM_STR 搭配

<?php

$db->quote('über', PDO::PARAM_STR ' PDO::PARAM_STR_NATL);

Additional emulated prepares debugging information for PDO

7.2 版的PDOStatement::debugDumpParams() 函數會把原始的SQL給印出

包括取代 bind param 的 SQL 語句

Support for extended operations in LDAP

跟 LDAP 不熟XD

以下是新增的常數跟方法

Address Information additions to the Sockets extension

PHP 7.2 加入 socket address info 的新函數

主要是對應到 C 的 getaddrinfo

新增的函數有

  • socket_addrinfo_lookup()
  • socket_addrinfo_connect()
  • socket_addrinfo_bind()
  • socket_addrinfo_explain()

這項新功能是在 2016-08-08 時提出

並依據當時提案實作出的功能

PHP RFC: Implement socket_getaddrinfo()

IPv4 Example

<?php
$addrinfo = socket_addrinfo_lookup('localhost', 2000, array('ai_family' => AF_INET, 'ai_socktype' => SOCK_STREAM));
$sockaddr = reset($addrinfo);
if (!$sockaddr) die ("No Valid Socket Types");
$sock = socket_addrinfo_bind($sockaddr);
// ^^ $sock is a socket resource that is bound to 127.0.0.1:2000 using TCP/IP ready for reading

var_dump(socket_addrinfo_explain($sockaddr));
/* Outputs:
array(5) {
  ["ai_flags"]=>
  int(0)
  ["ai_family"]=>
  int(2)
  ["ai_socktype"]=>
  int(1)
  ["ai_protocol"]=>
  int(6)
  ["ai_addr"]=>
  array(2) {
    ["sin_port"]=>
    int(2000)
    ["sin_addr"]=>
    string(9) "127.0.0.1"
  }
}
*/

IPv6 Example

<?php
$addrinfo = socket_addrinfo_lookup('localhost', 2000, array('ai_family' => AF_INET6, 'ai_socktype' => SOCK_STREAM));
$sockaddr = reset($addrinfo);
if (!$sockaddr) die ("No Valid Socket Types");
$sock = socket_addrinfo_bind($sockaddr);
// ^^ $sock is a socket resource that is bound to [::1]:2000 using TCP/IP ready for reading

var_dump(socket_addrinfo_explain($sockaddr));
/* Outputs:
array(5) {
  ["ai_flags"]=>
  int(0)
  ["ai_family"]=>
  int(10)
  ["ai_socktype"]=>
  int(1)
  ["ai_protocol"]=>
  int(6)
  ["ai_addr"]=>
  array(2) {
    ["sin6_port"]=>
    int(2000)
    ["sin6_addr"]=>
    string(3) "::1"
  }
}
*/

Parameter type widening

PHP 7.2 可以在實作 interface 的時候忽略參數的型態

<?php

interface A
{
    public function Test(array $input);
}

class B implements A
{
    public function Test($input){} // type omitted for $input
}

依據官方的說法

就算忽略參數的型態

依然符合 OOP 的 LSP 規則

其實這樣說也對

LSP 原本就是在描述實作的行為要一致

但沒有說參數的型態一定要一樣

但參數型態不一致

實作的行為要一致也是滿奇怪

但依然可以遵守 LSP 規則是沒錯的

放寬限制讓使用者更自由使用是好事

Allow a trailing comma for grouped namespaces

可以將 use 的 namespace 整合起來

但我記得好像前幾版就有支援了

結果 7.1 真的沒支援

7.2
php > use Foo\Bar\{
php {     Foo,
php {     Bar,
php {     Baz,
php { };
php >

7.1
php > use Foo\Bar\{
php {     Foo,
php {     Bar,
php {     Baz,
php { };
PHP Parse error:  syntax error, unexpected '}', expecting identifier (T_STRING) or function (T_FUNCTION) or const (T_CONST) in php shell code on line 5

Parse error: syntax error, unexpected '}', expecting identifier (T_STRING) or function (T_FUNCTION) or const (T_CONST) in php shell code on line 5

proc_nice() support on Windows

就是這個函式可以支援在 Windows 上跑了

pack() and unpack() endian support

Enhancements to the EXIF extension

EXIF extension 開始支援讀取更大範圍的格式

所以增加了以下幾種新 format

  • Samsung
  • DJI
  • Panasonic
  • Sony
  • Pentax
  • Minolta
  • Sigma/Foveon
  • AGFA
  • Kyocera
  • Ricoh
  • Epson

New features in PCRE

7.2 在 PCRE 加入 J modifier

可允許相同的 Group name

這裡有範例

SQLite3 allows writing BLOBs

SQLite3::openBlob() 可以在寫入模式開啟Blob

過去只能在讀取模式時開啟

Oracle OCI8 Transparent Application Failover Callbacks

加入 Oracle Database Transparent Application Failover (TAF) callbacks

TAF 是一個 Oracle 提供的”高可用性”功能

可以讓 PHP 在斷線的時候重新跟 Oracle 連線

以下是官方的範例

<?php

// Define userspace callback
class MyClass {
    public static $retry_count;
    public static function TAFCallback($conn, $event, $type)
    {
        switch ($event) {
            case OCI_FO_BEGIN:
                printf(" Failing Over ... Please stand by");
                printf(" Failover type was found to be %s \n",
                       (($type==OCI_FO_SESSION) ? "SESSION"
                        :($type==OCI_FO_SELECT) ? "SELECT"
                        : "UNKNOWN!"));
                self::$retry_count = 0;
                break;
            case OCI_FO_ABORT:
                // The application cannot continue using the database
                printf(" Failover aborted. Failover will not take place.\n");
                break;
            case OCI_FO_END:
                // Failover completes successfully. Inform users a failover occurs.
                printf(" Failover ended ... resuming services\n");
                break;
            case OCI_FO_REAUTH:
                printf(" Failed over user ... resuming services\n");
                // Replay any ALTER SESSION commands associated with this connection
                // eg. oci_parse($conn, 'ALTER SESSION ...') ;
                break;
            case OCI_FO_ERROR:
                // Stop retrying if we have already attempted for 20 times.
                if (self::$retry_count >= 20)
                    return 0;
                printf(" Failover error received. Sleeping...\n");
                sleep(10);
                self::$retry_count++;
                return OCI_FO_RETRY; // retry failover
                break;
            default:
                printf("Bad Failover Event: %d.\n", $event);
                break;
        }
        return 0;
    }
}

$conn = oci_connect('hr', 'welcome', 'localhost/XE');
$fn_name = 'MyClass::TAFCallback';

oci_register_taf_callback($conn, $fn_name); // Register TAFCallback to Oracle TAF 

$sql = "SELECT col1 FROM mytab";
$stmt = oci_parse($conn, $sql);
oci_define_by_name($stmt, 'COL1', $col1);

// For example, if a connection loss occurs at this point, oci_execute() will
// detect the loss and failover begins. During failover, oci_execute() will
// invoke the TAF callback function several times. If the failover is successful,
// a new connection is transparently created and oci_execute() will continue as
// usual. The connection session settings can be reset in the TAF callback
// function. If the failover is aborted, oci_execute() will return an error 
// because a valid connection is not available.

$e = oci_execute($stmt);
if ($e == false)
{
    // do error handling, if oci_execute() fails
    // var_dump(oci_error($stmt));
}
while (oci_fetch($stmt))
{
    echo "COL1 value is $col1<br>\n";
}

// do other SQL statements with the new connection, if it is valid
// $stmt = oci_parse($conn,  . . .); 

?>

Enhancements to the ZIP extension

支援讀取或寫入加密的 zip 檔案 (需要 libzip 1.2.0)

看更多