PHP7で出来るようになったことを独断と偏見で紹介してみる

はじめに

オープンストリームアドベントカレンダーの7日目です。

私は第一言語としてPHPを学んで以来、PerlやC#など他の言語に触れる機会もありつつも、メイン言語には未だにPHPを選ぶPHPerです。

エンジニア歴自体が2年そこそこなので、ディスられまくっていた時期のPHPをよく知らないのですが、2015年12月にリリースされたPHP7.0以降のPHPは、非常に面白い言語になってきたのではないかと思います。
PHP5時代には出来なかったいろいろな書き方が出来るようになってきています。

私もプライベートでは、出来るだけPHP7の書き方を取り入れて書くようにしています。
まだまだ使い切れていないものもありますが。
(なので、プロジェクトのPHP5.3に戸惑うという。。。)

そこで、PHP7になってから出来るようになったことを、 私の独断と偏見で 選んで紹介していきたいと思います。
選定基準は、私が普段から書いている書き方、もしくは 個人的に気になっているもの です。
なので、全部挙げている訳ではありません、ご了承ください。
主観マックスでいきます。

1. 型宣言(タイプヒンティング)と戻り値の型指定

  • 対象:PHP7.0~

http://php.net/manual/ja/functions.arguments.php#functions.arguments.type-declaration

PHP7といえば、やっぱりこれじゃないでしょうか。
PHP7.0から、関数のパラメータの型を縛る、静的言語のようなタイプヒンティングが出来るようになりました。
※正確に言えば、その前から出来たのですが、array型やクラス指定など、非常に限定的なものでした。

同時に、戻り値の型も縛ることが出来るようになりました。

1-1. 実例

どんな感じになるのか、実際のコードを見て頂いた方が早いでしょう。
入力文字列を任意の文字数分「x」でマスクする関数を作ってみました。

<?php
$str = 'hogefuga';
var_dump(mask($str, 4, true));

function mask(string $string, int $length, bool $isTail = false): string {
    if ($length < 0) return $string;
    $strLength = strlen($string);
    if ($length > $strLength) return $string;
    $mask = makeXStr($length);
    if ($isTail) {
        $ret = substr($string, 0, $strLength - $length) . $mask;
    } else {
        $ret = $mask . substr($string, -($strLength - $length));
    }
    return $ret;
}

function makeXStr(int $length): string {
    $mask = '';
    for ($i = 0; $i < $length; $i++) {
        $mask .= 'x';
    }
    return $mask;
}
// hogexxxx

型に合わない値が渡されてきた、あるいは戻り値の型が合わない場合、メソッドを呼んだ時点、もしくは戻り値を返そうとした時点でTypeErrorの例外がスローされます。

<?php
$str = 'hogehoge';
hogeFunc($str);

function hogeFunc(int $int): void {
    var_dump($int);
}
// PHP Fatal error:  Uncaught TypeError: Argument 1 passed to hogeFunc() must be of the type integer, string given

なので、意図しない値が渡されてきた、あるいは返ってきたせいで変な挙動に、ということを減らすことが出来ます。

1-1-1. 厳密な型チェック

ただし、デフォルトの設定では、string => intなど、 キャスト出来る場合は自動的にキャスト されます。
なので、下記のようなコードは動きます。

<?php
$num = '1';
hogeFunc($num);

function hogeFunc(int $int): void {
    var_dump($int);
}
// int(1)

こういうのですら許したくない場合は、declare文を用いてstrict_types宣言をすると、型まで厳密にチェックされます。
「==」と「===」みたいなものと思えば良いですね。

<?php
declare(strict_types=1);
$num = '1';
hogeFunc($num);

function hogeFunc(int $int): void {
    var_dump($int);
}
// PHP Fatal error:  Uncaught TypeError: Argument 1 passed to hogeFunc() must be of the type integer, string given

なお、strict_types宣言による厳密な型チェックは、宣言したファイルからの呼び出し のみ に適用されます。
定義元で厳密な型チェックがオンになっていても、 呼び出し元で宣言していなければ、厳密な型チェックは行われません。

1-2. メリット

私は、普段からこの書き方を取り入れていまして、出来るだけこれを書くようにしています。
理由は。。

  1. 単純に想定外の型を渡して欲しくない
  2. 処理の中身を見ずとも、パラメータで必要な型が分かる
  3. PhpStormなどでPHPDocumentorを作るのが楽になる
  4. PhpStormなどで、関数補完時に必要な型を表示してくれる
  5. 値のチェックを簡素化出来る

特に「3」については、PhpStormの方で型も変数名もどんどん補完してくれるので、後は説明を足すだけになります。
「4」も、パラメータ何渡さないといけないのか、入力欄に出てきてくれるので、非常に楽です。
圧倒的に便利。

「5」については、地味に有難い機能です。
例えば、先述のマスク関数の$isTailのチェックは、型宣言するだけで終了します。

function mask(string $string, int $length, bool $isTail = false): string {
    if ($length < 0) return $string;
    $strLength = strlen($string);
    if ($length > $strLength) return $string;
    $mask = makeXStr($length);
    if ($isTail) {
        $ret = substr($string, 0, $strLength - $length) . $mask;
    } else {
        $ret = $mask . substr($string, -($strLength - $length));
    }
    return $ret;
}

本来ならば、booleanであるか、というチェックを入れて、もしそうでないなら…という処理を自前で書かないといけません。
それが、型宣言すればあっという間に実現出来ます。
このようにして、値のチェックをある程度簡素化出来るのも有難いところです。

LaravelやSymfonyのような、オブジェクトを引数や戻り値に多用するフレームワークでは、その値の中身を一旦確認しておかないと変なところでハマりそうなので、そこだけは注意です。
ただそれも、PHP7.2で導入するobject型(1-7で後述)によって解決されるかもしれません。

1-3. Null許容も出来ます

  • 対象:PHP7.1~

通常はNullが許容されませんが、型の前に「?」を付けることでNullが許容出来るようになります。

<?php
$hoge = null;
$fuga = fugaFunc($hoge);
var_dump($fuga);

function fugaFunc(?string $fuga): ?string {
    return $fuga;
}
// NULL

どうしてもNullを扱わないといけない、という時には、型を縛らない、という選択肢もありますが、型を縛りつつもNullだけは許容する、というこちらの選択肢を個人的には取りたいです。

1-4. 使用可能な型

タイプヒンティングで指定可能な型は以下の通り。

  • string
  • int
  • array
  • bool
  • float
  • callable
  • クラス名 / インターフェイス名
  • iterable (PHP7.1~)
  • void (戻り値のみ。PHP7.1~)

1-5. void

  • 対象:PHP7.1~

戻り値が特に無いことを意味する「void」もPHP7.1から使えます。

<?php
$hoge = new Hoge();
$hoge->setFuga('fuga');

class Hoge {

    private $fuga;

    public function setFuga(string $fuga): void {
        $this->fuga = $fuga;
    }
}

voidを宣言した場合、どちらかを行う必要があります。

  • returnを省略
  • 空のreturnを書く

「空のreturn」とはreturn;とだけ書くことです。

setterメソッドでは、サンプルコードのように明示的に付けちゃうことが多いです。
まさかPHPでもvoidという文字列を見ることになろうとは。

このvoidさんですが、注意点が実は2つあります。

1-5-1. Nullを返すことは出来ない

戻り値が特に無いからvoidなんじゃないの、と思うのですが、 php.netにはしっかり書いてあります
http://php.net/manual/ja/migration71.new-features.php

void 関数から NULL を返すことはできません。

<?php
$hoge = new Hoge();
$hoge->setFuga('fuga');

class Hoge {

    private $fuga;

    public function setFuga(string $fuga): void {
        $this->fuga = $fuga;
        return null;
    }
}
// PHP Fatal error:  A void function must not return a value (did you mean "return;" instead of "return null;"?)

Nullを返そうとするとFatalが発生します。
代わりに空のreturn使え、と書いてありますね。

1-5-2. コンストラクタに使うとFatalになる

1回やらかしました。
次のようなコードがあります。

<?php
$hoge = new Hoge('hoge');
$hoge->setFuga('fuga');

class Hoge {

    private $hoge;
    private $fuga;

    public function __construct(string $hoge): void {
        $this->hoge = $hoge;
    }

    public function setFuga(string $fuga): void {
        $this->fuga = $fuga;
    }
}
// PHP Fatal error:  Constructor Hoge::__construct() cannot declare a return type

コンストラクタで戻り値の型を宣言するな 、と。
なので、コンストラクタだけはPHP5同様に戻り値の型を宣言しない

public function __construct(string $hoge) {
    $this->hoge = $hoge;
}

こういう書き方が良いようです。

1-6. iterable型

  • 対象:PHP7.1~

PHP7.1からタイプヒンティングで使えるようになったのが、このiterable型です。
foreachで回すことの出来るオブジェクトであることを示す疑似型です。
Traversableである、ってやつですね。
すぐに思いつくものだと、PDOのStatementオブジェクトがそうですね。
イテレータなどをまだちゃんと使ったことが無いので、これを機に使ってみたいです。

1-7. object型

  • 対象:PHP7.2~

2017/11/30に、PHP7.2.0がリリースされました。超タイムリー。
http://php.net/archive/2017.php#id2017-11-30-1

PHP7.2.0からは、タイプヒンティング出来る型として上記の型に加えて、object型もサポートされるようになりました。
クラス名指定によるオブジェクトのタイプヒンティングはPHP5時代から出来ましたが、もっと汎用的なオブジェクトに対してタイプヒンティングが出来るようになってます。

PHP 7.2.0 comes with numerous improvements and new features such as
・Object typehint

2. Null合体演算子

  • 対象:PHP7.0~

Nullチェックが楽に出来る書き方です。
もうisset()を使って、三項演算子で書く必要は無くなったのです。

$hoge = isset($_POST['fuga']) ? $_POST['fuga'] : $nyashi;

これ↑が、

$hoge = $_POST['fuga'] ?? $nyashi;

こう↑なります。同じ変数を2回書く必要が無くなりました。
本当に便利なので、PHP5の時より圧倒的に「?」を打つ回数が増えました。

3. 宇宙船演算子

  • 対象:PHP7.0~

名前がカッコいい。
2つの式を比較するための演算子で、 戻り値が3種類ある ちょっと特殊な演算子です。

  • 左辺が大きい:戻り値「1」
  • 右辺が大きい:戻り値「-1」
  • 両辺が等しい:戻り値「0」

switch文辺りとは相性が良さそうです。

<?php
switch (2 <=> 1) {
    case 1:
        echo '左辺の方が大きいです';
        break;
    case -1:
        echo '右辺の方が大きいです';
        break;
    case 0:
        echo '両辺が等しいです';
        break;
}
echo "\n";
// 左辺の方が大きいです

3-1. 宇宙船演算子による文字列比較

数字の比較は分かりやすくていいのですが、 文字列比較にも使えます
要は、型の比較表に則っていれば良いようです。

アルファベットの順列比較とかに使えないかなと。

<?php
$a = 'A';
$b = 'B';
switch ($a <=> $b) {
    case 1:
        echo $a . 'の方が後に来ます';
        break;
    case -1:
        echo $b . 'の方が後に来ます';
        break;
    case 0:
        echo '同じ文字です';
        break;
}
echo "\n";
// Bの方が後に来ます

4. list()が[]で書けるようになった

  • 対象:PHP7.1~

PHP7.1から出来るようになった書き方です。
1つのメソッドから複数の値が返ってくるような時、PHPではlist()を使って受け取っていました。
他の言語だとタプルとか呼んでるやつです。

これが、list()を使わず配列の省略記法[]を使って書けるようになっちゃいました。

<?php
list($hoge, $fuga) = nyashiFunc();

function nyashiFunc(): array {
    $hoge = 'hoge';
    $fuga = 'fuga';
    return [$hoge, $fuga];
}

これ↑が、

<?php
[$hoge, $fuga] = nyashiFunc();

function nyashiFunc(): array {
    $hoge = 'hoge';
    $fuga = 'fuga';
    return [$hoge, $fuga];
}

こう↑なります。

list()自体は他にも、配列の中身を別々の変数に展開したい時などにも使えるのですが、それも[]に置き換え可能です。

地味変ドリクスですが、シンプルになるのは良いことです。

4-1. 派生

ちなみに上記の省略記法はforeachでも使えるそうで。
というかこんな書き方出来るの初めて知りました。場合によってはめっちゃ便利。

<?php
$array = [
    ['hoge', 'HOGE'],
    ['fuga', 'FUGA'],
    ['nyashi', 'NYASHI']
];
foreach ($array as [$lower, $upper]) {
    echo sprintf('%s => %s%s', $lower, $upper, "\n");
}
// hoge => HOGE
// fuga => FUGA
// nyashi => NYASHI

5. list() (あるいは[])にキー指定が出来るようになった

  • 対象:PHP7.1~

PHP7.1から、list()及び[]で変数の中身を受け取る時、キーを指定した書き方が出来るようになりました。

<?php
$array = [
    [
        'name' => 'Miho Nishizumi',
        'team' => 'ankou'
    ],
    [
        'name' => 'Anzu Kadotani',
        'team' => 'kame-san'
    ]
];

foreach ($array as $info) {
    ['name' => $name, 'team' => $team] = $info;
    echo sprintf('name: %s, team: %s%s', $name, $team, "\n");
}
// name: Miho Nishizumi, team: ankou
// name: Anzu Kadotani, team: kame-san

さいごに

こう見てみると、PHP7.0以降から、だいぶいろいろ追加されたのだな、と思いました。
改めて調べてみて「こんな書き方も出来たのか」と気付かされたものも多いです。

今回はほとんど検証出来ませんでしたが、PHP7.2で更にいろいろ追加されていて、面白くなっていますので、ぜひチェックしてみてください。
http://php.net/manual/ja/migration72.new-features.php

アドベントカレンダーまでに間に合いませんでした

「抽象メソッドのオーバーライド」や「PDO のデバッグ情報にプリペアのエミュレートの内容を追加」、「ZIP 拡張モジュールの機能追加」あたりは実証して、後日改めて記事にしてみたいです。

マイナーバージョンアップでも新機能が増えるPHPの今後が楽しみです。

明日は@sshinobu さんです。