残り一年! PHP4からPHP5への移行

第3回オブジェクトの仕様変更の続きとインタフェース

前回に引き続き、オブジェクト仕様の変更について解説します。PHP5になってからオブジェクト指向プログラミング機能が強化され、Javaなどのオブジェクト思考言語に慣れたユーザにはより使いやすい言語仕様を持つようになりました。まれとは思いますが、PHP5からの新しい機能であるインターフェースも移行時に問題となる可能性も考えられるので、定義済みインターフェースも紹介しています。インターフェースとはメソッド定義がない抽象クラスのようなものです。よく分からない場合はプロパティ・メソッドの中身がないクラスのようなものと考えてください。

特に記述がない場合、PHP4はPHP4.4.x、PHP5はPHP5.2.xを意味します。

定義済みクラス

モジュールをロードするとクラスが定義される場合があるので、環境により定義済みクラスは異なります。ここではデフォルトのXAMMP for Windows 1.6.2のPHP4とPHP5の定義済みクラスの一部を紹介します。

PHP4の定義済みクラス
C:\xampp\php\php4>phpcli.exe -r "print_r(get_declared_classes());"
Array
(
    [0] => stdClass
    [1] => __PHP_Incomplete_Class
    [2] => Directory
    [3] => COM
    [4] => VARIANT
    [5] => swfshape
    [6] => swffill
以下省略
PHP5の定義済みクラス
C:\xampp\php>php.exe -r "print_r(get_declared_classes());"
Array
(
    [0] => stdClass
    [1] => Exception
    [2] => ErrorException
    [3] => COMPersistHelper
    [4] => com_exception
    [5] => com_safearray_proxy
    [6] => variant
    [7] => com
    [8] => dotnet
    [9] => ReflectionException
    [10] => Reflection
    [11] => ReflectionFunctionAbstract
    [12] => ReflectionFunction
    [13] => ReflectionParameter
    [14] => ReflectionMethod
    [15] => ReflectionClass
    [16] => ReflectionObject
    [17] => ReflectionProperty
    [18] => ReflectionExtension
    [19] => DateTime
    [20] => DateTimeZone
    [21] => LibXMLError
    [22] => __PHP_Incomplete_Class
    [23] => php_user_filter
以下省略

ここでは省略していますが、get_declared_classes関数の戻り値を参照すると、PHP5には例外クラス、リフレクションAPI関連クラス、XML関連クラスなどが定義されていることが分かります。

PHP5からはInterfaceもサポートしています。このためInterfaceとして定義された名前もクラス名として利用できません。

C:\xampp\php>php.exe -r "print_r(get_declared_interfaces());"
Array
(
    [0] => Traversable
    [1] => IteratorAggregate
    [2] => Iterator
    [3] => ArrayAccess
    [4] => Serializable
    [5] => Reflector
    [6] => RecursiveIterator
    [7] => OuterIterator
    [8] => Countable
    [9] => SeekableIterator
    [10] => SplObserver
    [11] => SplSubject
)

PHP5のデフォルトの定義済みクラスで、最も注意が必要と思われるクラス名はDateTimeクラスです。

例:同じクラス名の利用
<?php
class DateTime{}

$obj = new DateTime;
?>
実行例:
Fatal error: Cannot redeclare class DateTime in C:\xampp\php\- on line 2

どのクラス/インターフェース名でも同じですが、定義済みクラス名またはインターフェース名を利用しようとすると、Fatalエラーとなりスクリプトの実行が停止します。同じクラス名を定義している場合、クラス名を変更しなければなりません。

クラス/メソッド名の取得

クラス名を返すget_class/get_parent_class関数、メソッド名を返すget_class_methodsは、定義された際の名前をそのまま返すように仕様が変更されました。つまりPHP4ではクラス/メソッド名はすべて小文字に変換されてから返されましたが、PHP5では小文字への変換は行われません。

例:クラス名の取得
<?php
class ClassName {}
$obj = new ClassName;

echo get_class($obj);
?>
PHP5
ClassName
PHP4
classname

PHP4用のコードをPHP5で動作させるには

strtolower(get_class($obj))

と大文字を小文字に変換します。

PHPが自動的に設定する__CLASS___, __METHOD__, __FUNCTION__定数も定義されたままの値を持つように変更されています。

インスタンスとクラス名の比較

PHP4にはインスタンスがどのようなクラスから生成されているか確認するis_a関数、is_subclass_of関数が用意されていました。

例:オブジェクトのクラス名を比較
<?php
class foo {}
class bar extends foo {}
$obj = new bar;
if (is_a($obj, 'foo')) {
   echo '$obj is "foo"'; ←$objはbarクラスなので実行されない
}
if (is_subclass_of($obj, 'foo')) {
   echo '$obj is subclass of "foo"'; ←$objはfooを継承しているbarクラスのオブジェクトなので実行される
}
?>

PHP5でもis_a関数、is_subclass_of関数は利用できますが、新しくinstanceof演算子が追加されました。instanceof演算子はis_a関数とis_subclass_of関数を合わせた動作をします。

例:instanceof演算子の利用
<?php
class foo {}
class bar extends foo {}

$obj = new bar;
if ($obj instanceof foo) {
   echo '$obj is "foo"'; ←実行される
}
if ($obj instanceof bar) {
   echo '$obj is "bar"'; ←実行される
}
?>

get_class関数と異なりクラス名の比較は大文字・小文字を無視して行います。PHP5でのis_a関数、is_subclass_of関数の利用は推奨されていません。しかし、PHP5への移行だけであればコードの書き換えは必要ありません。将来的にはinstanceof演算子への書き換えは行ったほうがよいでしょう。

メソッド定義

PHP5からメソッドへのアクセスにpublic, protected, privateのアクセス制御が可能になりました。

<?php
class foo {
  // fooだけがアクセス可能
  private func1() {}
  // fooとfooを継承しているクラスからアクセス可能
  protected func2() {}
  // どこからでもアクセス可能
  public func3() {}
  func4() {} ←省略するとpublic
}
?>

PHP5への移行だけであればコードの書き換えは必要ありません。エラーが発生することもありません。

プロパティ定義

PHP4からPHP5への移行の障害となるため、PHP5.1.3から新しく設けられたエラーレベルE_STRICTを設定してもvar宣言でプロパティを宣言してもエラーを発生しなくなりました。現在のPHP5ではvar宣言はpublic宣言と同等に扱われます。

例:
<?php
class foo {
  public $p;

  // PHP5ではvarはpublicと同じ
  var $v;
}
?>

PHP5への移行には書き換えの必要はありません。現在のPHP5ではエラーが発生することもありません。

空のオブジェクト

PHP4のオブジェクト変数はプロパティが定義されていない場合、

empty($object)

とするとTRUEが返ってきました。PHP5のオブジェクト変数はempty($object)で常にFALSEと評価されます。

例:PHP4とPHP5と実行結果が異なる
<?php
class foo {}
$obj = new foo;
if (!$obj) {
  echo 'PHP4 is used'; // PHP4の場合のみ実行される
}
?>

オブジェクトプロパティの有無を調べるにはget_object_vars関数を利用します。

例:
<?php
class foo {}
$obj = new foo;
if (!get_object_vars($obj)) {
  echo 'Works for PHP4 and PHP5';
}
?>

最初の回で解説済みですが、PHPのバージョンをチェックするために言語仕様の違いで確認する必要はありません。PHP_VERSION定数とcompare_version関数で確認できます。

配列形式のプロパティアクセス

古いPHPではオブジェクト変数のプロパティは配列のようにアクセスできましたが、現在のPHP4、PHP5ではアクセスできません。さらに、PHP5ではFatalエラーになり実行が停止します。

例:PHP5ではFatalエラー
<?php
class foo {
  var $a = 1;
  var $b = 2;
}

$obj = new foo;

var_dump( $obj['a'] );
?>
実行例:PHP4
[yohgaki@dev php4-php5-migration]$ ./php4 object-array-syntax.php
NULL
実行例:PHP5
[yohgaki@dev php4-php5-migration]$ ./php5 object-array-syntax.php
Fatal error: Cannot use object of type foo as array in /home/yohgaki/php4-php5-migration/object-array-syntax.php on line 9

オブジェクトの比較

PHP5からオブジェクト変数がハンドルで取り扱われるようになり、オブジェクト変数を比較するとPHP4とPHP5で結果が異なる場合があります。

例:オブジェクト変数の比較
<?php
class foo {}
class bar extends foo {}

$foo1 = new foo;
$foo2 = $foo1;

if ($foo1 == $foo2) {
  echo '$foo1 == $foo2';  ←PHP4/PHP5共に実行
}
if ($foo1 === $foo2) {
  echo '$foo1 === $foo2'; ←PHP4/PHP5共に実行
}

$foo1->var = 123; ←PHP4の場合、$foo1と$foo2は別の変数。PHP5は同じ変数。
if ($foo1 == $foo2) {
  echo '$foo1 == $foo2';  ←PHP5のみ実行
}
if ($foo1 === $foo2) {
  echo '$foo1 === $foo2'; ←PHP5のみ実行
}
?>

PHP4、PHP5の両方ともデータ型を考慮しない比較演算子"=="が利用された場合、プロパティを比較しすべて値が同じ場合はTRUEを返します。PHP5でデータ型を考慮する比較演算子"==="を利用した場合、ハンドルで識別されるのでPHP4とPHP5で比較の結果が異なります。

PHP5でPHP4と同じ動作にするには、cloneで別のオブジェクト変数として

$foo2 = clone $foo1;

とすればよいように思えますが、===演算子でFALSEと評価されるので

if ($foo1 === $foo2) {
  echo '$foo1 === $foo2'; ←PHP4/PHP5共に実行
}

の部分の実行結果が異なってしまいます。cloneを利用すると$foo1と$foo2は別のオブジェクト変数になり、PHP5ではecho文は実行されません。

PHP4のコードでオブジェクトが代入でコピーされることを前提に記述されているコードの場合、修正が必要な個所を見つけることが難しい場合も多いです。

次回以降で詳しく解説しますが、php.iniのzend.ze1_compatibility_mode設定はオブジェクトの代入と比較の動作に影響します。

クラスの結合

PHP4まではクラスとオブジェクトを結合させるaggregate関数がサポートされていましたが、PHP5からはサポートされません。JavaScriptやRubyのように既存のオブジェクトを拡張できたので利用されていた方もいるかもしれません。残念ながら代替する方法はPHP5に用意されていません。PHP5のクラスはJavaなどの静的な言語と同様に設計・実装しなければなりません。

クラス定義は静的な定義のみ可能ですが、プロパティの追加はいつでも行えます。この仕様に変更はありません。

例:
<?php
class foo {}
$obj = new foo;
$obj->new_property = 123;
?>

インターフェースを利用する場合の定義順序

インターフェースはPHP5からの新しい機能なので、PHP4からPHP5への移行時に問題になることはないです。しかし、動的言語としては直感的に分かりづらい動作をするので解説しておきます。

PHP5からクラスAPIのインターフェースを定義するinterfaceが導入されました。interfaceとはどのようなメソッドを実装しなければならないか定義する機能で、クラスのAPI(インターフェース)を定義するために利用します。

interfaceはメソッドの中身を定義できない抽象クラスのようなものに見えますが、制約があります。インターフェースを利用するにはあらかじめinterfaceを定義しておかなければ、実行時にFatalエラーになります。

例:interface定義がないのでエラー
<?php
// インターフェースが定義されていないのでエラー
$bar = new bar;

interface foo{} // 空の定義
class bar implements foo {}
?>
実行例
Fatal error: Class 'bar' not found in /home/yohgaki/- on line 3

インターフェースを利用する場合、インターフェース定義とその定義を実装するクラスは利用される前に定義されていなければなりません。前の例をプログラマの意図通りに動作させるコードは以下のようになります。

例:エラーが発生しないインターフェースの利用
<?php
interface foo{} // 空の定義
class bar implements foo {}

// インターフェース、それを利用するクラス共に
// 定義されてるので動作する
$bar = new bar; 
?>

__toStringの呼び出し

PHP5からオブジェクト変数が文字列として利用された場合、__toStringメソッドが呼び出されるようになりました。__toStringメソッドが未定義の場合、補足可能なFatalエラー例外が発生します。

例:__toStringが未定義のオブジェクト変数を文字列として利用
<?php
class foo {}
$obj = new foo;
echo $obj;
?>
実行例:PHP4
[yohgaki@dev php4-php5-migration]$ ./php4 __toString.php
Object
実行例:PHP5
[yohgaki@dev php4-php5-migration]$ ./php5 __toString.php 
Catchable fatal error: Object of class foo could not be converted to string in /home/yohgaki/php4-php5-migration/__toString.php on line 4

まとめ

今回はオブジェクトの仕様変更で最も大きな変更であったオブジェクト変数のハンドル化や、PHP5で追加されたオブジェクト指向プログラミングサポート機能とPHP4のオブジェクト指向プログラミングサポート機能の違いを解説しました。

次回はモジュール関連の違いについて解説します。

おすすめ記事

記事・ニュース一覧