めっきり寒い毎日ですが、皆さんいかがお過ごしでしょうか。1月はあっという間に終わってしまい、2月に突入しました。2月は逃げる、3月は去ると言いますが、時間が過ぎる速度が年々増しているような気がする今日このごろです。春は早く来てほしいのですが、時間が過ぎ去ってしまうのは寂しいものです。
きみ、それPHP 4やで
すっかり感傷にふけったところで、診断のお時間です。今回のコードは2004年に書かれたものです。2004年といえばPHP 4真っ盛りの時代ですね。年代もののコードを扱うことが多い本連載ですが、このコードは今も現役で(PHP 4環境で!)稼働しているというのがこれまでと違う点です。えー、わかります、言いたいことはわかります。PHP 4は、もはやメンテナンスが行われていないので、もしセキュリティホールがあったとしても、どうしようもありません。攻撃され放題です。早急にPHP 5へのアップグレードを行っていただきたいところです。
今回は、PHP 5にアップグレードするにあたって、このコードをどう変えていくのがよいか、という視点で診断を行ってみます。
診断するのはログイン処理を行うlib/Login.php
とcontroller/LoginController.php
の2ファイルです。では、PHP 4ながら現役バリバリのコードを見てみましょう。さあ、お入りください。
controller/LoginController.php
<? php
require_once ( 'lib/BasicController.php' );
require_once ( 'lib/CheckExecuter.php' );
require_once ( 'lib/Login.php' );
require_once ( 'model/UserModel.php' );
define_once ( 'LOGIN_ID' , 'login_id' );
define_once ( 'PASSWORD' , 'password' );
class LoginController extends BasicController {
var $NEXT_PAGE = 'top.php' ;
function LoginController () {
$this -> init ();
unset ( $_SESSION [ SESSION_USER ]);
if ( Http :: isPost ()) {
$this -> values = $_POST ;
} else {
$this -> values = null ;
}
}
function execute () {
if ( Http :: isPost ()) {
$checker = new CheckExecuter ( $this );
$checker -> check ();
$this -> errorMessages = $checker -> getErrorMessages ();
if (! $this -> isError ()) {
$_SESSION = array ();
$userModel = new UserModel ();
$userModel -> setLoginId ( $this -> values [ LOGIN_ID ]);
$userModel -> setPassword ( $this -> values [ PASSWORD ]);
if ( Login :: auth ( $userModel )) {
unset ( $_SESSION [ SESSION_USER ]);
$_SESSION [ SESSION_USER ] = $userModel ;
$this -> transferPage ( $this -> NEXT_PAGE );
} else {
$this -> addErrorMessage ( 'error_not_login' );
return ;
}
}
}
}
function checkLoginId () {
if ( isset ( $this -> values [ LOGIN_ID ])
&& $this -> values [ LOGIN_ID ]) {
return null ;
}
else {
return "check_login_id" ;
}
}
function checkLoginId () {
if ( isset ( $this -> values [ LOGIN_ID ])
&& $this -> values [ LOGIN_ID ]) {
return null ;
}
else {
return "check_login_id" ;
}
}
function checkPassword () {
if ( isset ( $this -> values [ PASSWORD ])
&& $this -> values [ PASSWORD ]
&& ereg ( '^[a-zA-Z0-9]+$' , $this -> values [ PASSWORD ])
&& strlen ( $this -> values [ PASSWORD ]) <= 8 ) {
return null ;
}
else {
return "check_password" ;
}
}
}
?>
lib/Login.php
<? php
require_once ( 'lib/Log.php' );
require_once ( 'lib/Database.php' );
class Login {
function Login () {
}
function auth (& $userModel ) {
if ( get_class ( $userModel ) != 'usermodel' ) {
return false ;
}
$handleName = addslashes ( $userModel -> getHandleName ());
$password = addslashes ( $userModel -> getPassword ());
$where = sprintf ( 'handle_name=\'%s\' and password =\'%s\'' , $handleName , $password );
$db =& Database :: getInstance ();
if (( $record = $db -> getOneRecord ( '*' , 'v_user' , $where ))) {
$userModel -> setId ( $record [ 'id' ]);
$userModel -> setIdCardNo ( $record [ 'id_card_no' ]);
$userModel -> setEmail ( $record [ 'email' ]);
$userModel -> setFirstName ( $record [ 'first_name' ]);
$userModel -> setLastName ( $record [ 'last_name' ]);
$userModel -> setFirstNameKana ( $record [ 'first_kana' ]);
$userModel -> setLastNameKana ( $record [ 'last_kana' ]);
$userModel -> setPrefecture ( $record [ 'prefecture' ]);
$userModel -> setAddress ( $record [ 'address' ]);
$userModel -> setSubAddress ( $record [ 'sub_address' ]);
$userModel -> setZip ( $record [ 'zip' ]);
$userModel -> setTel ( $record [ 'tel' ]);
$userModel -> setBirthdayYear ( $record [ 'birthday_year' ]);
$userModel -> setBirthdayMonth ( $record [ 'birthday_month' ]);
$userModel -> setBirthdayDay ( $record [ 'birthday_day' ]);
$userModel -> setLangCode ( $record [ 'lang_code' ]);
return true ;
} else {
return false ;
}
}
}
?>
診断
1. PHP 4らしいコード
まずコードを見た印象としては、実にPHP 4らしいコードだな、ということを感じました。
ざっと見渡しただけでも、require_once
文による外部PHPファイルの読み込み、メンバ変数のvar
キーワードによる定義、クラス名と同名のコンストラクタ、メソッドのアクセス修飾子がないなどの特徴があります。また、引数の参照渡しやstatic
指定なしのクラスメソッドなどもそうですね。
これらは、PHP 4では問題のない書き方だったのですが、時を経て、変化したPHP 5にはそぐわない書き方になりました。これは言語の変化だけではなく、開発手法や利用するライブラリ、フレームワークが変化したというのもその一因です。こうした環境の変化に合わせて、コードの書き方を変えていくことで、PHPやその周辺ツールの良さをより享受することができます(PHP 5の中でも、5.3未満とそれ以降でまた異なるのですが、それはまた別の機会に) 。
では、それぞれの個所について見てみましょう。
1-1. require_once
文による外部ファイルの読み込み
両コードとも、ファイルの先頭でrequire_once
文で外部のクラスファイルを読み込んでいます。当時、複数のクラスファイルで構成されるアプリケーションでは定石だったこの方法ですが、オートローダが普及した今では、オートローダ自身や設定ファイルなど最低限のファイルのみ自分で読み込んで、あとはオートローダに自動で読み込ませるという手法を取ることが多くなりました。
require_once
文は悪くない方法なのですが、ファイル位置を相対パスで指定したり、include_path
を別途指定する必要があるなど、少し煩わしさがあります。オートローダを使えば物理的なファイルパスを気にすることなくクラスファイルを読み込むことができるので便利です。
オートローダは自作することもできますが、Composer[1] のオートローダを利用するのが簡単です。Composerのオートローダを利用することで、ほかのライブラリやフレームワークを利用する際も同じオートローダを使うことができ、連携しやすい構成となります。
1-2. 名前空間
Composerによるオートローダを導入して、ほかのライブラリやフレームワークと組み合わせる際に重要になるのが、名前空間の設定です。複数のソフトウェアを組み合わせると、同じ名前のクラスがそれぞれに存在して、競合してしまう場合があります。これはソフトウェア同士だけではなく、開発した時点ではオリジナルなクラス名でも、同名のクラスがPHP言語に導入されてしまう場合もあります(最近では、PHP 5.4からSessionHandler
クラスが導入されて、アプリケーションに存在するクラスと競合するということがありました) 。
名前空間を導入しておくと、こうした名前の競合を避けることができます。たとえば、下記のように同じUser
クラスでも、名前空間をApp
とFooLib
のように別にしておけば、名前空間を含めた完全なクラス名は\App\User
、FooLib\User
となり、同居することできます。
namespace App ;
class User {}
namespace FooLib ;
class User {}
1-3. クラス名と同名のコンストラクタ
コンストラクタのメソッド名がクラス名と同じ名前になっています。
PHP 4のコンストラクタはこのように記述するしかなかったのですが、PHP 5では、__construct
というメソッド名がコンストラクタとして記述できるようになりました。従来の同名コンストラクタも利用できるのですが、PHP 5.3.3以降、名前空間が設定されたPHPファイルでは同名コンストラクタが呼ばれなくなっているので、__construct
を使いましょう。
1-4. オブジェクトの参照渡し
PHP 4らしい書き方と言えば、このオブジェクトの参照渡しです。たとえば、Login
クラスのauth()
メソッドの引数が参照渡しとなっています。
function auth (& $userModel ) {
また、インスタンスを変数に代入する場合も下記のように参照で受け取るように書かれています。
$db =& Database :: getInstance ();
PHP 4では、変数の代入やメソッド呼び出しで引数を渡す際、デフォルトは値渡しとなっていました。そのため、オブジェクトを代入する際は処理速度やメモリ効率を考慮して、参照渡しを行うことがTipsとして知られていました。しかし、PHP 5からは、オブジェクトの実体ではなく、参照のみが渡されるようになったため、パフォーマンスを考慮した参照渡しを行う必要がなくなりました。
また、Login
クラスのauth()
メソッドの場合、引数で受け取ったUserModel
クラスのオブジェクトに対してセッターメソッドで値をセットしています。このオブジェクトの変更を呼び出し元でも利用するため、参照渡しで引数を渡しています。これもPHP 5であれば、前途のとおりオブジェクトの参照を渡すので、メソッド内のオブジェクトと呼び出し元のオブジェクトは同一のオブジェクトとなります。よって、この理由による参照渡しを行う必要もありません。
このようにPHP 5でオブジェクトを代入する際、&
を使った明示的な参照渡しは必要ありません。特にパフォーマンスについては、参照渡しを行うことでかえってパフォーマンスが落ちるケースもあるので、&
による参照渡しは基本的には使わないほうがよいです。
1-5. メソッドの重複
LoginController
クラスを見てみると、checkLoginId()
というメソッドが2つ定義されています。PHP 5では、1つのクラスに同じ名前のメソッドは定義できないのですが、PHP 4ではこういった実装が認められていました。同じ名前のメソッドを2つ書くメリットは特にないと思うので、おそらくコピペしてそのまま放置されたのでしょう。
1-6. get_class()関数の挙動の違い
Login
クラスのauth()
メソッドでは、処理の先頭で引数のクラスをチェックしています。get_class()
関数でクラス名を取得して、期待すべきクラスでなければ、return
するようになっています。
PHP 4では、このget_class()
関数の戻り値はクラス名を小文字にした文字列でした。よって、このコードでは期待すべきクラス名UserModel
を小文字にしたusermodel
と比較しています。しかし、PHP 5ではクラス名がそのまま返ってくるので、戻り値はUserModel
となります。つまり、このままこのコードをPHP 5で実行すると、$userModel
変数がUserModel
クラスのオブジェクトでも、この式はtrue
になりません。
PHP 5で動かすなら、比較文字列をUserModel
にするか、get_class()
の戻り値をstrtolower()
関数で小文字に変換する必要があります。
単に引数の型をチェックしたいということであれば、PHP 5のタイプヒンティングを使う方法もあります(下記) 。こちらのほうが意図がわかりやすいコードになりますね。
function auth ( UserModel $userModel ) {
1-7. ereg()は非推奨
LoginController
クラスのcheckPassword()
メソッドで使われているereg()
関数は、PHP 5では非推奨(E_DEPRECATED)となっています。これはpreg_match()
関数で代用しましょう。
ereg ( '^[a-zA-Z0-9]+$' , $this -> values [ PASSWORD ])
↓
preg_match ( '/^[a-zA-Z0-9]+$/' , $this -> values [ PASSWORD ])
1-8. クラスメソッドにstaticが付いていない
Login
クラスのauth()
メソッドは、LoginController
クラスでの呼び出し方を見るとクラスメソッドを想定しているようです。
if ( Login :: auth ( $userModel )) {
このようにクラスメソッドとして呼び出しているメソッドの定義にstatic
キーワードが付いていない場合、Strictエラー(Non-static method Foo::hello() should not be called statically)が発生します。
コードを読む際もメソッドがインスタンスメソッドなのかクラスメソッドなのかの判別ができないので、クラスメソッドの定義にはstatic
キーワードを付けておきしょう。
2. addslashes()によるエスケープ処理
PHP 4固有のことではないのですが、気になる個所があります。
Login
クラスのauth()
メソッドでは、データベースへの問い合わせが実行されているのですが、検索条件を組み立てている個所で値のエスケープにaddslashes()
関数が使われています。addslashes()
関数はマルチバイト文字などを考慮せずに機械的にエスケープを行ってしまうため、利用する文字エンコーディングによっては、文字列が壊れてしまったり、本来は無効化すべき特殊文字をエスケープできない場合があります。
SQLに含める値をエスケープするにはaddslashes()
関数ではなく、データベースドライバに定義されている専用のエスケープ関数(PostgreSQLならpg_escape_string()/pg_escape_literal()、MySQLならmysqli_real_escape_string()など)を用いるほうが安全です。また、もし可能なのであれば、PDO(PHP Data Object)を使い、プリペアドステートメントで値をバインドする方式に変えるほうがよりよいでしょう。
addslashes()
関数を使う場面はおおよそないとも言えるので、この関数が登場した場合は要注意です。
3. 連続したセッターメソッド
これは何かマズイことがあるというわけではないのですが、実装について気になった個所です。
Login
クラスのauth()
メソッドでは、$userModel
変数に対してセッターメソッドが15行ほど並んでいます。各セッターメソッドに対して連想配列$record
の対応するキーの値をひたすら渡しています。まあ丁寧でよいのですが、冗長なので、下記のように連想配列ごと引数で渡して、UserModelクラス内で値を一括でセットしてくれるようなヘルパーメソッドが欲しくなるところです。
$userModel -> setAttributes ( $record );
今回は、主にPHP 4の書き方に注目して治療するポイントを見てきました。このポイントをベースに、次回はPHP 5らしい書き方に治療していきます。お楽しみに。