PHPで型を意識し安全なプログラムを書く

#はじめに
PHPは動的型付けであり、弱い型付けの言語です。
[各言語の型の特徴をまとめてみた](https://qiita.com/kaitaku_san/items/148d1491596a58b9c97d)

昔はPHPはちょっとしたホームページを簡単にちゃちゃっと作るものだったので
緩くてよかったんだと思いますが、
昨今はPHPでの大型システム構築も多いので、
緩い型の仕様だとバグのリスクが大きいです。

そんな感じで、
PHPでも型をちゃんと意識していこうぜっていう風潮が強いですね。

実際、PHPのバージョンアップでも
型の緩い仕様を改善するような機能追加が多いです。

PHPでも、
最近追加されているいろいろな機能をちゃんと意識して利用すれば
ある程度型安全なプログラムを書くことが可能です。


この記事では、

* PHPの型の特徴
* PHPで型を意識した安全なプログラムを書くために利用すべき機能

を紹介します。


#PHPの型の特徴
そもそもPHPにはどのような型があるのかについてはこちらを参照ください。
[PHPの型一覧　変数の型とタイプヒンティングできる型は別](https://qiita.com/minato-naka/items/abd7f76dca2b15a7d6ba)


##変数の型宣言をしない
PHPでは変数の型が自動で決まります。
変数に文字列を入れればその変数はstring型になるし、
整数を入れればint型になります。

変数を作るときに型を定義する必要がありません。

例

```php
$string = 'aaa';     //文字列を入れる
var_dump($string);   //string(3) "aaa"  //stging型になる

$int = 123;          //整数を入れる
var_dump($int);      //int(123)  //int型になる

$array = ['aaa'];    //配列を入れる
var_dump($array);    //array(1) {[0]=>  string(3) "aaa"}  //array型になる
```

PHPしか使ったことがない人からすると当たり前に感じるかもしれませんが、
例えばJavaなどほかの言語では、
まず初めに変数宣言をして、その時にその変数の型を決めてしまいます。

```java
int integer;     // int型の変数integerを宣言
String str;     // String型の変数strを宣言
```

その後はその決めた型のデータしか変数に入れることはできません。
※明示的にキャストすれば型変更可能
　
　
##暗黙的な型変換
PHPではプログラムの文脈に応じて自動で型を変換して処理をしてくれる
`暗黙的な型変換`があります。

例（[PHP: 型の相互変換 - Manual](https://www.php.net/manual/ja/language.types.type-juggling.php)）

> \$foo = "1";  // \$foo は文字列 (ASCII 49) です
> \$foo *= 2;   // ここでは、\$foo は整数 (2) です
> \$foo = \$foo * 1.3;  // ここでは、\$foo はfloat (2.6) です
> \$foo = 5 * "10 Little Piggies"; // \$foo は整数 (50) です
> \$foo = 5 * "10 Small Pigs";     // \$foo は整数 (50) です

つまり、最初にstring型のデータを変数に入れていたのに、
いろいろな処理を通った結果
知らぬ間にintになってたりfloatになっていたりするってことですね。
　
##PHPの型のメリット・デメリット
これらのPHPの特徴は、
よく言えば
型を意識しなくてもPHPが勝手にいい感じにいろいろやってくれるので
特に初心者には優しく、簡単にプログラミングすることができます。
　


ただ、デメリットとしては
どの変数がどの型になっているのか明確ではないため
想定外のバグを生んでしまうリスクなります。
（ほかの言語専門のプログラマがよく文句言ってる。）
　
　
メリット、デメリットありますが、
PHPでもできるだけ型を意識した安全なプログラムを書いたほうがいいという意見が多いですね。
　
　
#型を意識したPHPプログラミングの機能
PHPで型を意識したプログラミングをすることができる機能として
この5つを簡単に紹介します。

* 厳密な比較演算子（===）を使う
* 比較系関数の厳密比較オプションをtrueにする
* 引数の型宣言をする
* 返り値の型宣言をする
* strict_types=1を指定する



##厳密な比較演算子（===）を使う
PHPには普通の比較演算子（==）と厳密な比較演算子（===）があります。

普通の比較演算子では、
比較対象が別々の型だとしても、
自動的に型を変換して比較を行います。

この仕様は便利な場合もありますが、
想定外のバグを生むことも多いです。

例

```php
// 普通の比較
var_dump('aaa' == 'aaa');  // bool(true)
var_dump('aaa' == 'bbb');  // bool(false)
var_dump(123 == 123);      // bool(true)
var_dump(123 == 999);      // bool(false)


// 文字列と数値の比較
// これは便利な場合も多い
var_dump('123' == 123);    // bool(true)


// これはバグを生みそう
var_dump('123abc' == 123); // bool(true)
var_dump('abc' == 0);      // bool(true)
```

一番下の2つの様な例は、
多くの場合trueになることを期待していないと思います。

このような想定外の比較結果が出ることを防ぐために
厳密な比較演算子を利用します。

厳密な比較演算子は、
値だけでなく型も含めて一致するかどうかを判定します。

先ほどの例でtrueになってしまった
文字列と数値の比較もちゃんとfalseとなります。

```php
var_dump('123' === 123);    // bool(false)
var_dump('123abc' === 123); // bool(false)
var_dump('abc' === 0);      // bool(false)
```






##比較系関数の厳密比較オプションをtrueにする
PHPの組み込み関数には、
[in_array](https://www.php.net/manual/ja/function.in-array.php)など比較を行う関数がいくつかあります。

それらの関数の内部では
デフォルトでは普通の比較演算子（==）で比較が行われているため
先ほど書いた文字列と数値の比較などをする際に
想定外の比較結果になってしまうことがあります。

in_arrayの例を見てみます。

```php
var_dump(
    in_array('xxx', ['aaa', 'bbb', 'ccc'])
);
// bool(false)

var_dump(
    in_array(123, ['aaa', 'bbb', '123ccc'])
);
// bool(true)
```

厳密な比較を行っていないため、
2つ目の例がtrueになってしまいます。


このような事象を防ぐために、
オプション引数を渡すことで厳密な比較を行うことができます。

```php
var_dump(
    in_array(123, ['aaa', 'bbb', '123ccc'], true)
);
// bool(false)
```

先ほどこの例はtrueになってしまっていましたが、
in_arrayの第3引数に`true`を渡すことによって
内部で厳密比較を行ってくれました。

その結果、比較対象の型が違うためfalseとなっています。

ほかにも
[array_search](https://www.php.net/manual/ja/function.array-search.php)や[array_keys](https://www.php.net/manual/ja/function.array-keys.php)など、
デフォルトでは普通の比較を行うが
オプション引数を渡すことで厳密比較をしてくれる関数がいくつかあります。

何か比較処理を行うようなPHP組み込み関数を利用する際には、
厳密比較を行うオプションを指定できないかどうか確認し、
可能であればオプション指定するようにするといいです。






##引数の型宣言をする
PHPでは、
関数の引数として受け入れるデータ型を指定することができます。

例えば、このような整数を2倍にして返す関数。

```php
function doubleInt($int) {
    return $int * 2;
}

echo doubleInt(5);  // 10
```
　
　
この関数では整数のみを引数として受け取りたいので、
このように型宣言をします。

```php
function doubleInt(int $int) {  // ←この引数部分にintと記述すると型宣言できる
    return $int * 2;
}

echo doubleInt(5);      // 10
echo doubleInt('aaa');  // error
```

これで、
int型の`5`を引数で渡した場合は正常に動作しますが、
string型の`'aaa'`を渡した場合はエラーになります。

このように引数の型を宣言することで、
想定外の型の引数を渡されないのでバグのリスクを減らせます。

また、関数内で受け取った引数の型チェックのロジックなどを書く必要もなくなるので
コードもシンプルにすることができます。

型宣言で指定可能な型一覧をこちらで把握しておくことをお勧めします。
[PHP: 関数の引数 - Manual](https://www.php.net/manual/ja/functions.arguments.php#functions.arguments.type-declaration)





##返り値の型宣言をする
PHPでは、
関数の返り値の型を指定することができます。

例えば、先ほどの`doubleInt()`関数では
返り値はint型である想定なので、そのように型宣言をします。

```php
function doubleInt(int $int): int {  // ←この部分で「: データ型」の形で返り値の型宣言をする
    return $int * 2;
}

echo doubleInt(5);  // 10
```
　
もし宣言した型と違う型を返した場合はエラーが発生します。

```php
function doubleInt(int $int): int {
    return 'aaa';                    // 文字列'aaa'を返す
}

echo doubleInt(5);  // error
```

この戻り値の型宣言をすることによって、
関数を複雑に作りこんだ時などに
自分の想定していないデータ型の値を返してしまう
ようなことを防ぐことができます。

また、
関数を使う側からしても、
返却されるデータの型が保証されている状態なので
安心して関数を使うことができます。

[PHP: 返り値 - Manual](https://www.php.net/manual/ja/functions.returning-values.php#functions.returning-values.type-declaration)





##strict_types=1を指定する
先ほど紹介した
引数の型宣言と返り値の型宣言で、
「宣言した型と違う型を渡す（返す）とエラーになる」
と言いましたが、
実は正確には間違っています。
　
　
PHPのデフォルトでは **弱い型付け** の状態になっていて、
この状態だと
宣言した型と違う型を渡した（返した）としても
可能な限りは自動で型変換を行って処理を進めてしまいます。

```php
function doubleInt(int $int) {  // 引数はint型で型宣言されている
    return $int * 2;
}

echo doubleInt('5');   // 10  // string型の'5'を渡してもint型に自動変換されて処理される 
echo doubleInt(true);  // 2   // bool型のtrueを渡してもint型の1に自動変換されて処理される
```

このように、引数をint型として型宣言していたとしても、
string`'5'`やbool`true`はエラーとならずintに自動変換されて処理が行われます。
※string`'aaa'`やarray、objectなどintに自動変換するのが無理な値の場合はエラーとなります
　
　

このような状態を防ぐために
declaer命令で`strict_types=1`を指定することで
**強い型付け**の状態にすることができます。

方法は、PHPファイルの一番最初に
`declare(strict_types=1);`を1行書くだけです。
　
　

これでPHPが**強い型付け**の状態となり、
先ほどのような宣言と違う型のデータを渡した（返した）場合には
必ずエラーが発生するようになります。

```php
declare(strict_types=1);  // 強い型付けの設定

function doubleInt(int $int) {
    return $int * 2;
}

echo doubleInt('5');      // error
echo doubleInt(true);     // error
```

弱い型付けの状態では厳密な型チェックが行われないため
想定外のバグを生むリスクがあります。

基本的には`stryct_types=1`を指定する習慣としたほうがいいです。


#まとめ
PHPで型を意識し安全なプログラムを書くための機能として
5つ紹介しました。

* 厳密な比較演算子（===）を使う
* 比較系関数の厳密比較オプションをtrueにする
* 引数の型宣言をする
* 返り値の型宣言をする
* strict_types=1を指定する

もしほかにも何かあれば
コメントで教えていただけるとありがたいです。


#おまけ
PHP7.4ではプロパティの型宣言ができるようになる見込みらしいです。
[【PHP7.4】ついにPHPにプロパティ型指定がやってくる](https://qiita.com/rana_kualu/items/9b4502d229d19635ef34)


#参考
[独習PHP](https://www.amazon.co.jp/dp/B01FH3KVNU/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)
[in_arrayを使うときは黙って第三引数を付けること](https://qiita.com/ritukiii/items/3a6add378ae089ab5d70)
[PHPの型と型安全について(PHP7からのPHPプログラミング)](https://qiita.com/bokotomo/items/1ead446ef689dc45b487)
[[PHP] 型宣言(PHP7)](https://qiita.com/hikaru_/items/2c12068eafea75e51cce)
[PHP7でタイプセーフなPHPを書きたい時](https://qiita.com/itosho/items/f7d1b3ab41bce6503b52)