PHPで各種バリデーション

# `$_GET` `$_POST` などの要素の **未定義** や **想定外の型** の検出

[「$_GET, $_POSTなどを受け取る際の処理」](http://qiita.com/mpyw/items/2f9955db1c02eeef43ea) で詳しく解説しています。

```text:受け取るクエリ
a=foo&b[]=bar
```

```php:PHPコード(キャストなし)
$a = filter_input(INPUT_GET, 'a'); // "foo"
$b = filter_input(INPUT_GET, 'b'); // false
$c = filter_input(INPUT_GET, 'c'); // null
```

```php:PHPコード(キャストあり)
$a = (string)filter_input(INPUT_GET, 'a'); // "foo"
$b = (string)filter_input(INPUT_GET, 'b'); // ""
$c = (string)filter_input(INPUT_GET, 'c'); // ""
```

ここから先はこういったチェックを行った後の状態であると仮定して，値の形式に関するバリデーションのみに着目します。

```php
/* 1. 未定義や想定外の型の検出 */
$email = (string)filter_input(INPUT_POST, 'email');

/* 2. 値の形式に関するバリデーション */
if (false !== filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "正しいEメールアドレスです: $email\n";
} else {
    echo "Eメールアドレスの形式が不正です: $email\n";
}
```

なお，この例において，無効なメールアドレスを変数 `$email` に格納しなくてもよい場合，

```php
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

if (is_string($email)) {
    echo "正しいEメールアドレスです: $email\n";
} else {
    echo "Eメールアドレスの形式が不正です\n";
}
```

としてまとめてしまっても構いません。

**※ 送信されていない場合に`null`になるので `false !==` の比較は不適当です。**

|関数|フィルタリング対象| 成功時の返り値 | 失敗時の返り値| インデックス<br />未定義時の返り値|
|:----:|:----:|:----:|:----:|:----:|:----:|
|[filter_var](http://www.php.net/manual/ja/function.filter-var.php)| 自分で渡す値 | 文字列や整数など<br />(フィルタによる) | `false` | - |
|[filter_input](http://www.php.net/manual/ja/function.filter-input.php)| `$_GET` `$_POST` などの<br />インデックス| 文字列や整数など<br />(フィルタによる) | `false` | `null` |

# URL

2通りの実装パターンを示します。多くの場合では **B** を選択しておけばよいでしょう。

```php:A
function is_valid_url($url)
{
    return false !== filter_var($url, FILTER_VALIDATE_URL);
}
```

```php:B
function is_valid_url($url)
{
    return false !== filter_var($url, FILTER_VALIDATE_URL) && preg_match('@^https?+://@i', $url);
}
```

| | A | B |
|:--|:--:|:--:|
| http://<br />https:// | ○ | ○ |
| mailto: | ○ | × |
| その他のスキーム:// | ○ | × |
| その他のスキーム: | × | × |

# Eメールアドレス

3通りの実装パターンを示します。不変ではないIPアドレスで登録されたら後々いろいろ面倒なことが起きそうなので， **C** を選択しておくのが無難だと思います。

**※ 追記により良い結論を掲載しているので必ずご覧ください**

```php:A
function is_valid_email($email)
{
    return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
}
```

```php:B
function is_valid_email($email)
{
    switch (true) {
        case !is_string($email):
        case false === $pos = strrpos($email, '@'):
        case false === filter_var(substr($email, $pos + 2, -1), FILTER_VALIDATE_IP):
        case false === filter_var(substr_replace($email, '0.0.0.0', $pos + 2, -1), FILTER_VALIDATE_EMAIL):
            return false;

        default:
            return true;
    }
}
```

```php:C
function is_valid_email($email)
{
    return false !== filter_var($email, FILTER_VALIDATE_EMAIL) && ']' !== substr($email, -1);
}
```

| | A | B | C |
|:--|:--:|:--:|:--:|
| ローカル部@ドメイン | ○ | ○ | ○ |
| ローカル部@[ **IPv4** ] | ○ | ○ | × |
| ローカル部@[ **IPv6** ] | × | ○ | × |

[「PHPしか書けないザコがメールアドレス正規表現でガチ勢に挑んでみた」](http://qiita.com/mpyw/items/257eabe0b43b1e02e6f7) でさんざん議論しましたが，PHP純正のフィルタリング関数をうまく活用するのが最も賢明な方法かな･･･という思いで執筆させていただきました。ええ，もちろん日本の携帯キャリアのRFCガン無視実装は弾いてますけどね･･･

### 2015.8.11 追記

- 最近ではメールアドレスの国際化が進み，ローカル部のバリデーションに関してはほぼ無意味になって来ています。しかし現状それを考慮しているWebサービスが少ないということもあって，そういったメールアドレスを使用している人は非常に少ないはずなので，周囲の様子を見ながら対応する…ぐらいでもいいのかもしれません。
- ドメインも同様ですが，**DNSレコード**をチェックすることによってドメインの存在を確認出来ます。

これらを踏まえて，以下に[@xRxさんのコード](http://qiita.com/ShibuyaKosuke/items/0b9a8fddaefb2060a14a#comment-7ae19685fded414b6fab)に少し手を加えたものを示します。**C**をベースにしています。第2引数に`true`を渡すとDNSレコードをチェックします。

```php:C+DNSレコードチェック
function is_valid_email($email, $check_dns = false)
{
    switch (true) {
        case false === filter_var($email, FILTER_VALIDATE_EMAIL):
        case !preg_match('/@([^@\[]++)\z/', $email, $m):
            return false;

        case !$check_dns:
        case checkdnsrr($m[1], 'MX'):
        case checkdnsrr($m[1], 'A'):
        case checkdnsrr($m[1], 'AAAA'):
            return true;

        default:
            return false;
    }
}
```

もしUTF-8文字列にも対応したい場合は，[egulias/EmailValidator](https://github.com/egulias/EmailValidator)ではなく[symfony/Validator](https://github.com/symfony/Validator)を使うようにしてください。ソースコードを見る限り前者は**`MX`レコードのチェックしか行っていません**。後者は内部で前者を利用しているようですが，わざわざこのためだけに修正を行っています。

- [egulias/EmailValidator src/Egulias/EmailValidator/EmailValidator.php #L149](https://github.com/egulias/EmailValidator/blob/af864423f50ea59f96c87bb1eae147a70bcf67a1/src/Egulias/EmailValidator/EmailValidator.php#L149)
- [symfony/Validator Constraints/EmailValidator.php #L155](https://github.com/symfony/Validator/blob/master/Constraints/EmailValidator.php#L155)
 
Symfonyのコンポーネントを使ったことがないのでサンプル紹介は割愛します。

### 2015.8.11 追記

**PHP7.1.0から，ネイティブでUTF-8文字列を含むEメールアドレスのバリデーションに対応しました。**このバージョン以降では最も適切な選択肢と言えそうです。

```php:2017.1.24現在，最も推奨される実装
function is_valid_email($email, $check_dns = false)
{
    switch (true) {
        case false === filter_var($email, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE):
        case !preg_match('/@([^@\[]++)\z/', $email, $m):
            return false;

        case !$check_dns:
        case checkdnsrr($m[1], 'MX'):
        case checkdnsrr($m[1], 'A'):
        case checkdnsrr($m[1], 'AAAA'):
            return true;

        default:
            return false;
    }
}
```

注意点として，ドメイン部はPunycode表記になっている必要があります。例えばGigazineの[「Gmailが日本語など非アルファベット文字を含むメールアドレスとの送受信に対応」](http://gigazine.net/news/20140806-gmail-admit-non-alphabet/)では`武@メール.グーグル`が例に挙げられていますが，これは**`武@xn--4dkua4c.xn--qcka1pmc`**にエンコードされている必要があります。

# 郵便番号や住所

1. [日本郵便のHP](http://www.post.japanpost.jp/zipcode/dl/kogaki.html) から全国一括のCSVデータをダウンロードする。
2. MySQLやSQLiteにインポートして使う。

`SELECT` 一発で判定出来るデータがあるので，下手に正規表現を書こうとするよりもSQLを使うべきであると言えるでしょう。

# 電話番号

[総務省のHP](http://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/number_shitei.html) のデータをもとに，目的にあった関数を自分で作成します。正規表現一発でやろうとしても，正規表現が長すぎて失敗することが多いので注意してください。必要があればMySQLやSQLiteにインポートして使います。

```php:手抜き実装
function is_valid_phone_number($number)
{
    return is_string($number) && preg_match('/\A\d{2,4}+-\d{2,4}+-\d{4}\z/', $number);
}
```

真面目にやろうとすると [かなり面倒](http://qiita.com/mpyw/items/431c0c8cb70084a74be5) です。

# 整数

## 全てを整数型に強制する

### 整数へのキャスト

```php:全てを10進数として解釈
$id = (int)$id;
/**
 *  "1"    => 10進数の "1"   => 1
 *  "01"   => 10進数の "01"  => 1
 *  "010"  => 10進数の "010" => 10 
 *  "0x1A" => 10進数の "0"   => 0 
 */
```

```php:全てを10進数として解釈し，0～9の範囲に必ず収める
$id = min(max((int)$id, 0), 9);
```

### [intval](http://www.php.net/manual/ja/function.intval.php) 関数

```php:全てを10進数として解釈
$id = intval($id); // 10がデフォルト
/**
 *  "1"    => 10進数の "1"   => 1
 *  "01"   => 10進数の "01"  => 1
 *  "010"  => 10進数の "010" => 10 
 *  "0x1A" => 10進数の "0"   => 0 
 */
```

```php:全てを2進数として解釈(先頭に何もつけてはならない)
$id = intval($id, 2);
/**
 *  "1"    => 2進数の "1"   => 1
 *  "01"   => 2進数の "01"  => 1
 *  "010"  => 2進数の "010" => 2
 *  "0x1A" => 2進数の "0"   => 0
 */
```

```php:全てを36進数として解釈(先頭に何もつけてはならない)
$id = intval($id, 36);
/**
 *  "1"    => 36進数の "1"    => 1
 *  "01"   => 36進数の "01"   => 1
 *  "010"  => 36進数の "010"  => 36
 *  "0x1A" => 36進数の "0X1A" => 42814
 */
```

```php:全てを8進数として解釈(先頭の0はあってもなくてもOK)
$id = intval($id, 8);
/**
 *  "1"    => 8進数の "1"  => 1
 *  "01"   => 8進数の "1"  => 1
 *  "010"  => 8進数の "10" => 8
 *  "0x1A" => 8進数の ""   => 0
 */
```

```php:全てを16進数として解釈(先頭の0xはあってもなくてもOK)
$id = intval($id, 16);
/**
 *  "1"    => 16進数の "1"   => 1
 *  "01"   => 16進数の "01"  => 1
 *  "010"  => 16進数の "010" => 16
 *  "0x1A" => 16進数の "1A"  => 26
 */
```

```php:10進数・8進数・16進数を自動判定して解釈
$id = intval($id, 0); // 0は自動判定を意味する
/**
 *  "1"    => 10進数の "1"  => 1
 *  "01"   => 8進数の  "1"  => 1
 *  "010"  => 8進数の  "10" => 8
 *  "0x1A" => 16進数の "1A" => 26
 */
```

## 正しい場合は整数型に変換し，誤っている場合は `false` とする

### [filter_var](http://www.php.net/manual/ja/function.filter-var.php) 関数

```php:厳密な10進数のみを許可して解釈
$id = filter_var($id, FILTER_VALIDATE_INT);
/**
 *  "1"    => 10進数の "1" => 1
 *  "01"   => false
 *  "010"  => false
 *  "0x1A" => false
 */
```

```php:厳密な10進数かつ0～9の範囲内のみを許可して解釈
$options = ['min_range' => 0, 'max_range' => 9];
$id = filter_var($id, FILTER_VALIDATE_INT, compact('options'));
```

```php:10進数・8進数・16進数を自動判定して解釈
$id = filter_var($id, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX);
/**
 *  "1"    => 10進数の "1"  => 1
 *  "01"   => 8進数の  "1"  => 1
 *  "010"  => 8進数の  "10" => 8
 *  "0x1A" => 16進数の "1A" => 26
 */
```