PHP7のタイプヒントの使い方

(Last Updated On: 2018年8月13日)

PHP7で基本的なデータ型である”int”や”float”、”array”タイプヒント(データ型のヒント)がサポートされます。使い方を間違えると思ってもいない問題が発生することがあります。しかし、正しく使えば問題ありません。タイプヒントの使い方を簡単に紹介します。

PHPのタイプヒント

タイプヒント自体は新しい機能ではありません。PHP5からオブジェクトのクラス名をタイプヒントとして利用できました。

<?php

class bar {}

function foo(bar $v) {
  var_dump($v);
}

$v = new bar;
foo($v);

PHP7では”int”などの基本的なデータ型にもこの構文が使えるようになります。

PHP7からは戻り値のタイプヒントも利用できます。

<?php

class bar {}

function foo(bar $v): bar {
  var_dump($v);
  return $v;
}

$v = new bar;
foo($v);

関数名の前にタイプヒントが無いのは少し違和感がありますが、これはHackと同じ構文になっているからです。

 

PHP7タイプヒントとデータ型

タイプヒントを正しく使うにはPHPのデータ型を理解する必要があります。PHPはデータ型が弱い言語なのでデータ型を意識しないで使ってきた方も多いと思います。基本的なデータ型のタイプヒントを利用する場合、データ型を意識して使用しなければなりません。

“int” – 整数型

PHPが動作するマシンのCPUによってデータ型が変わります。

  • 32 bit CPU – 符号付き32bit整数。
  • 64 bit CPU – 符号付き64bit整数。(PHP7からWindowsも64bit版は符号付き64bit整数になります)

符号付き32bit整数は凡そマイナス20億からプラス20億までの値しか保存できません。年齢や在庫数などを保存するには十分な大きさですが、範囲を気にしなくてもよいくらい大きな値とは言えません。十分に注意してください。

“float” – 倍精度浮動小数点

整数型と異り、CPUに関わらずIEEE 754 倍精度浮動小数点型になります。64bitを利用して数値を表現します。符号付き53bit整数の範囲まで整数を正確に表現することができます。

“array” – 配列型

PHPの配列型を指定します。配列型であれば内容に関わらず”array”タイプヒントを利用できます。

“bool” – 論理型

PHPの論理型を指定します。TRUE/FALSEが利用できます。

“string” – 文字列型

PHPの文字列型は実際にはバイナリ/バイト型と考えて構いません。特定の文字エンコーディングなどは文字列型としては強制しません。

 

PHP7タイプヒントのモード

PHP7のタイプヒントは2つ動作モードを持っています。

  • Weakモード – データ型の変換を許容するモード
  • Strictモード – データ型の変換を許可しないモード

Strictモードでは指定したデータ型に厳格に一致する必要があります。Weakモードではデータ型が変換されます。データ型の変換ルールは基本的に今までのPHPと同じですが、数値に不正な文字列がある場合、E_NOTICEを発生させます。この様に多少違う部分もありますが、基本的には同じだと考えて構いません。

 

PHP7タイプヒントのエラー動作

Weakモードのタイプヒントは指定されたデータ以外のデータ型の変数が渡されると

特に”int”、”float”タイプヒントを使った場合に問題が発生します。PHPのint型はJavaなどのlong型と異り、CPUによってデータ型が保存できる数値が変化し、32 bit CPUでは符号付き32 bit整数、64 bit CPUでは符号付き64bit整数になります。

つまり32 bit CPUの場合、”int”タイプヒントを利用すると符号付き32bit整数までしか表現できません。符号付き32bit整数を超える場合、TypeExceptionが発生します。

 

PHP7タイプヒントエラーの対処方法

整数型の範囲を超える数値が与えられた場合、TypeExceptionが発生します。これではページの途中で出力されたり、何も表示されず真っ白なページが表示されたりします。必ずユーザー定義エラーハンドラーを定義してTypeExceptionも処理するようにしなければなりません。

<?php
set_exception_handler(function($e) { echo $e->getMessage()."\n"; var_dump($e);} );

function foo(int $v) {
  echo $v;
}

// 大きすぎる数値を渡す
foo('9999999999999999999999999999');

出力

Argument 1 passed to foo() must be of the type integer, string given, called in - on line 9
object(TypeException)#2 (7) {
 ["message":protected]=>
 string(91) "Argument 1 passed to foo() must be of the type integer, string given, called in - on line 9"
 ["string":"BaseException":private]=>
 string(0) ""
 ["code":protected]=>
 int(1)
 ["file":protected]=>
 string(1) "-"
 ["line":protected]=>
 int(4)
 ["trace":"BaseException":private]=>
 array(1) {
 [0]=>
 array(4) {
 ["file"]=>
 string(1) "-"
 ["line"]=>
 int(9)
 ["function"]=>
 string(3) "foo"
 ["args"]=>
 array(1) {
 }
 }
 }
 ["previous":"BaseException":private]=>
 NULL
}

つまりタイプヒントを使う場合、例外のハンドルが必須となります。

PHP7には新しい内部関数パラメーターハンドラが利用されます。新しいハンドラは整数オーバーフローを検出し、オーバーフローした場合にE_WARNINGを発生させNULLを返します。

<?php
error_reporting(-1); //全てのエラーを有効にする

set_error_handler(
function ($errno, $errstr, $errfile, $errline)
{
    echo "ERROR [$errno] $errstr\n";
    echo "  Error on line $errline in file $errfile";
    echo ", PHP " . PHP_VERSION . " (" . PHP_OS . ")\n";

    /* Don't execute PHP internal error handler */
    return true;
}
);

// 大きすぎる数値を渡す - エラーハンドラで処理される
var_dump(mt_srand('99999999999999999999999'));


function foo(int $v) {
  echo $v;
}

// 不正な数値を渡す
foo('9999abcdedf99999999999');
// 大きすぎる数値を渡す - 例外ハンドラが無いので実行終了
foo('9999999999999999999999999999');

出力

ERROR [2] mt_srand() expects parameter 1 to be integer, string given
  Error on line 19 in file -, PHP 7.0.0-dev (Linux)
NULL
ERROR [8] A non well formed numeric value encountered
  Error on line 22 in file -, PHP 7.0.0-dev (Linux)
9999
Fatal error: Argument 1 passed to foo() must be of the type integer, string given, called in - on line 29 and defined in - on line 22

PHP7の内部関数は大きな値の場合、タイプヒントのようにオーバーフローを検出しエラーにします。例外ではなくエラーとなります。もしかするとリリースまでには例外になるかも知れません。

因みにPHP7以前のPHPでは引数のオーバーフローでエラーになりませんでした。オーバーフローする値はPHP_INT_MAX/PHP_INT_MINの値が設定されます。

http://3v4l.org/ASaDm

 

PHPが表現できる整数

PHPの整数型が32 bit CPUでは符号付き整数になってしまうことで困った事になります。PHPで整数が表現できる範囲は以下の通りです。

  • 符号付き32bit整数 : 32bit CPU 整数型(int)
  • 符号付き53bit整数 : 32/64bit CPU 浮動小数点型(float)
  • 符号付き64bit整数 : 64bit CPU 整数型(int)

タイプヒントを使ってポータブルな整数型パラメーターを持つ関数/メソッドが作れるのは符号付き32bit整数の範囲に限るか、浮動小数点型を使って53bit整数の範囲に限るかのどちらかしかありません。数値演算が必要ない場合、数値型のタイプヒントは使わずに”string”タイプヒントを使います。

 

PHP7の”int”タイプヒントの使い方

”int”タイプヒントは符号付き32bit整数の範囲までしか扱えない、として利用します。これはデータベースのレコードID(多くの場合は符号付き64bit整数)には利用できないことを意味します。例えば、特定のレコードを取得する関数には使えません。

間違い:intタイプヒントはポータブルでない

<?php
function getRecord(int $id) {
  // DBからレコードを取得
}

// 32bit 環境ではTypeException
$record = getRecord('9876543210');

正しい使い方:stringタイプヒントはポータブル

<?php
function getRecord(string $id) {
  // DBからレコードを取得
}

// 32bit 環境ではTypeException
$record = getRecord('9876543210');

“int”タイプヒントが使える場合は「確実に符号無し32bit整数の範囲内」にデータが収まる場合です。

  • 年齢
  • 在庫数(普通は20億を超えない)

このような数値に利用します。

 

PHP7の”float”タイプヒントの使い方

数値演算を行う場合に向いています。CPUの種別を問わず符号付き53bit整数までは正しく整数演算できます。ただし”float”は符号付き64bit整数より小さな範囲しか正しく表現できない点に注意が必要です。

 

PHP7の”string”タイプヒントの使い方

“string”タイプヒントは文字列に利用します。前述の通り、PHPの文字列型はバイナリ/バイト型です。”string”型にはどのようなデータでも格納されます。バリデーションが必要な場合はプログラマがバリデーションを行わなければなりません。

“string”タイプヒントはデータベースなど外部システムのデータで符号付き32bit整数、浮動小数点(倍精度)の範囲を超えるデータに利用します。

  • レコードID(普通は64bit整数)
  • NUMERIC/DECIMALのカラム
  • JSONの数値
  • XMLの数値

データ仕様を明確に理解し、他のデータ型(int/float)で扱えないものは”string”タイプヒントを利用するしかありません。

データベースのレコードIDなど(一旦、ブラウザに送信して戻って来たIDなど)は数字のみであることは保証されません。入力バリデーションで確実にチェックするようにします。SQLiteのように整数型カラムでも文字列を保存できてしまう物もあるので、注意が必要です。

参考:

 

PHP7のタイプヒント関連の問題点

内部関数のオーバーフロー検出とユーザー定義関数のオーバーフロー検出ではエラー/例外と異なります。

データベース、JSONなどから取得した数値データ(文字列として取得)がオーバーフローするかどうかはキャストすれば分かります。

<?php

$v = (int)'9999999999999999999999999999999';
if ($v === PHP_INT_MAX || $v === PHP_INT_MIN) {
  echo "Overflow";
}

PHP_INT_MAX/PHP_INT_MINは上限/下限なのでその値の場合はオーバーフローした、として取り扱うと簡単です。呼び出し側、呼び出された側、どちらでチェックするのが良いは「呼び出し側でチェックする」方が良いです。この方が呼び出された側とその先の関数が何度も繰り返しオーバーフローをチェックせずに済みます。

参考:エンジニア必須の概念 – 契約による設計と信頼境界線

PHP7から戻り値のタイプヒントも利用できます。整数型のパラメーターでも数値演算を行った場合、簡単に浮動小数点になってしまいます。例外が発生するので、例外に対してどのように対処するのか予め決めておく必要があります。

 

まとめ

知っておいて頂きたいことをすべては書ききれていませんが、基本的な部分は書けたと思います。足りない、間違っていると思える部分はコメントをお願いします!

投稿者: yohgaki