PHPのin_arrayは罠が多いので注意喚起が必要

## 最初に結論

`in_array`には第三引数`true`を指定しろ。絶対にだ。

## はじめに

`in_array`については、はじめに[PHP: in_array - Manual](http://php.net/manual/function.in-array.php)をよく読んでおいてほしい。

この記事に書いたコード断片は、どうか読むだけではなく自分で手を動かして確認してほしい。

PHPで短いコードを動かすのは[PsySH](http://psysh.org/)を利用すると、とても捗る。ローカルに動作環境がなければ、[Ideone.com](http://ideone.com/)などのオンラインサービスを利用しても差支ない。

```php
static $fruits = ["apple", "orange", "banana"];

in_array("apple", $fruits); // => true
in_array("mikan", $fruits); // => false
```

これが`in_array`のふつうの使ひかただ。

## Webアプリケーションでの実例

では、次のような例を見てみよう。`$_GET`から入力を受け取る、かなりシンプルなWebアプリケーションのような想定だ。

```php
<?php
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

static $ids = [0, 1, 2, 5, 8];

if (in_array($input_id, $ids)) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";
```

しかし、このコードには看過しがたいバグがある。`?id=0.0`と入力したとき、「idはありません」ではなく「id = 0.0」と表示されてしまふ。

状況を整理しよう。上記のコードをさらに簡略化したバージョンで説明する

### 何も入力しないとき

```php
/** $_GET["id"] がないとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は -1
in_array($input_id, [0, 1, 2, 5, 8]); // false
```

これは意図通りだ。ちゃんと弾けてる。

### 整数を入力したとき

```php
/** $_GET["id"] = "5" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "5"
in_array($input_id, [0, 1, 2, 5, 8]); // true
```

この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。

確認しておきたいのは、第一引数が文字列であること、第二引数は整数の配列であることだ。

### 小数を入力したとき

```php
/** $_GET["id"] = "5.0" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "5.0"
in_array($input_id, [0, 1, 2, 5, 8]); // true
```

もう一度聞くが、この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。

注目したいのは、もちろん最後の行だ。どうしてこうなるのか。`in_array`の比較は`==`演算子の結果と同じだ。

```php

 5  ==  5  ;  // true 当然
"5" == "5" ;  // true 当然


"5" ==  5  ;  // true これも
 5  == "5" ;  // true 良いよね

 5  == 5.0 ;  // true まあ
5.0 ==  5  ;  // true わかる

 5  == "5.0"; // true えっ
"5" == "5.0"; // true ええっ
```

これでもあなたは「PHPはシンプルな言語」だと呼び続けられるだろうか。

### 文字列

```php
/** $_GET["id"] = "apple" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "apple"
in_array($input_id, [0, 1, 2, 5, 8]); // true えええっ
```

簡略してない最初のバージョンに突っ込むと、「id = apple」と表示される。

例によって`==`演算子での比較を見てみよう。

```php
"apple" == 0 // true
0 == "apple" // true
```

あなたはこれを見ても、本当に「PHPはシンプルな言語」だとry

## 改善

では、どうすればサンプルのWebアプリケーションが改善されるか検討しよう。

### `==`を知る

PHPで仕事するなら[型の相互変換](http://php.net/manual/language.types.type-juggling.php)と[PHP 型の比較表](http://php.net/manual/types.comparisons.php)を穴が空くまで読んでおくことで、今回のような問題を「匂い」で察知することができるようになります。

### in_arrayに第三引数を渡す

> ## in_array
>
> (PHP 4, PHP 5)
> `in_array` — 配列に値があるかチェックする
>
> ### 説明
>
> ```php
> bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
> ```
>
> `needle` で `haystack` を検索します。 `strict` が設定されていない限りは型の比較は行いません。

*http://php.net/manual/ja/function.in-array.php 2015年2月8日の版より引用*

`in_array`には第三引数がある。型の比較をするとかしないとか表現がわかりにくいのだけれど、実際は`false`のときは `==` で、`true`のときは `===` で比較されると覚えておけばいい。

### 文字列が(十進)整数かどうか調べる

ここでは`0-9`の文字だけを使って十進数の整数を表現した文字列のことだ。

[PHP: PHP 型の比較表 - Manual](http://php.net/manual/ja/types.comparisons.php)には「注意: HTMLフォームは整数、浮動小数点数、booleanを渡してはくれず、文字列を渡します。文字が数値であるかどうか確認するには、 [is_numeric()](http://php.net/manual/ja/function.is-numeric.php)を使うとよいでしょう。」などと書いてあるのだが、実はそれでは不十分だ。上の「小数を入力したとき」のような場合で弾くことができない。

逆に、`is_numeric()`のチェックをせずに`int`にキャストすればいいじゃない、との発想もあるだろう。

```php
(int)"a"; // 0
(int)1.0; // 1
```

「大は小を兼ねる」の発想で「?id=1.0」と「?id=1」は同一視していいじゃないか、と見ることもできるかもしれない。しかし、筆者はWebアプリケーションとしては、意図しない状態は極力減らしておくべきだと考へる。SEOのような観点からも、ふとしたきっかけで無駄にクローリングされることになるかもしれず好ましい状態ではない。

ある文字列が整数を表現するかどうかを判定するには、以下のようなイディオムが有名だ。

```php
$input = "0";
$input_is_int = (string)$input === (string)(int)$input;
```

あるいは次のような正規表現でもチェックできる。自分の直感でわかりやすい方を選んでいい。*(性能差などは瑣末な問題だ)*

```php
$input = "0";
$input_is_int = preg_match('/\A(?:0|-?[1-9][0-9]*)\z/', 1) > 0;
```

次のような函数を用意しておけば、値が`int`でも`string`でも`float`でも汎用的に利用できるのでべんりかもしれない。

```php
/**
 * @param  int|float|string $value
 * @return boolean
 */
function is_intval($value)
{
    return !is_float($value) && (string)$value === (string)(int)$value;
}
```

### 改良バージョン

```php
<?php

$is_intval = function ($value)
{
    return !is_float($value) && (string)$value === (string)(int)$value;
};

$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
$input_id = $is_intval($input_id) ? (int)$input_id : $input_id;

static $ids = [0, 1, 2, 5, 8];

if (in_array($input_id, $ids, true)) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";
```

## そもそもの話

`in_array()`が良くないって意見もある。

以下のようなコードでは、`in_array()`バージョンほど厄介な問題はおこらない。 *(整数と小数の問題は健在)*

```php
static $ids = [
    0 => true,
    1 => true,
    2 => true,
    5 => true,
    8 => true,
];

if (isset($ids[$input_id])) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

```

また、`in_array()`は線形探索するため、配列の要素数によっては深刻な性能劣化を引き起こすことがあるだろう。

もちろん、ケースバイケースで良い点と悪い点が存在するのは当然のことだ。おのおののケースで`isset`と`in_array()`のどちらが良いかは読者への課題としたい。

## そのほかの話

なぜかストックが増えたことだし、せっかくなのでちょっぴり追記する。

* [`array_search()`](http://php.net/manual/ja/function.array-search.php)は`in_array`と同じ問題がある
* [`switch`](http://php.net/manual/ja/control-structures.switch.php)文も同様だが、`in_array()`と違ってstrictオプションに相当するものはない
  * 少々過激ではあるが、筆者は`switch`ではなく`elseif`で書くことを推奨したい
  * ちなみにPythonには`switch`に相当する構文は存在しない

## まとめ

筆者は`in_array()`が大好きだ。