新年あけましておめでとうございます。昨年暮れに始まった本連載ですが、みなさんの現場にあるPHPコードを改善すべく、診断を行ってきたいと思います。本年もよろしくお願いします!
PHPにはPHPの書き方がある
2014年最初の患者さんは、とあるECサイトにて動いていたコードです。ショッピングカートを表現するクラスで、書かれた時期は2000年ごろです。動作していたPHPのバージョンは、PHP 3の国際化版というオールドマニアには垂涎もののバージョンです。当時は、PHPが普及しだしたころで、まだ標準的なPHPコードの書き方というものがない状況でした。そのため、ほかの言語で培った流儀をそのまま持ってきて書かれたコードがよく見られました。
今回は、そんなほかの言語の書き方を持ってきてしまったコードです。さあ、お入りください。
診断
1. 文字エンコーディングがShift_JIS、改行コードがCRLF
このPHPコードは、文字エンコーディングがShift_JIS、改行コードがCRLFで記述されていました(この記事ではUTF-8で表記しています)。
PHPコードをShift_JISで書くことがただちにダメというわけではないのですが、正常に動作させるには、PHPのコンパイルオプションや設定を調整する必要があります。また、ファイルによって文字エンコーディングが異なると、コードの差分を取る際に、コード自体は同じなのに異なるコードとして検出されるなど、差分を確認することが難しくなります。これは改行コードについても同様です。
現在では、文字エンコーディングをUTF-8で記述することでほぼ不都合はないため、文字エンコーディングはUTF-8、改行コードはLFに統一して記述することが推奨されています。
もし携帯電話など一部の古いデバイス向けにShift_JISを利用せざるを得ない場合は、PHPコード自体はUTF-8で記述しておき、出力時にmb_output_handler()
やmb_convert_encoding()
を使って、UTF-8からShift_JISへ変換して出力するのがよいでしょう。
2. ファイル拡張子が.inc
このコードのファイル名は「cart.inc」です。この「.inc」という拡張子、昨今ではあまり見ないものですが、以前はよく用いられていました。この拡張子が付いたファイルは、外部に公開するものではなく、別のPHPファイルから読み込まれることを想定していました(includeだから.incですね)。後述するハンガリアン記法にも少し通じるのですが、拡張子にファイルの動作とは別な意味を持たせていました。
意図はわからないことはないですが、注意しておかないといけないことがあります。もし、このファイルをそのままDocumentRoot以下に置いておくと、外部から直接アクセスされた場合、なんとソースコードがそのまま出力されてしまいます! これは、Webサーバが.incという拡張子のファイルをPHPファイルとして認識していないためで、WebサーバがApacheなら、addHandler
やSetHandler
でPHPコードとして実行されるように設定を行う必要があります。
また、IDEなどの開発ツールでも.incがPHPファイルであることを認識していないものがあるので、これもまた設定が必要となります。
現在はこうしたPHPファイルの種別によって拡張子を変えるという慣習はなく、PHPコードが書かれたファイルであれば、すべて「.php」という拡張子にします。
3. これはPHP?
コードを見て目に付くのが、特徴的なコメントの書き方です。このコードを書いた方は、C言語やVisual Basicなどの別の言語を書いた経験があるそうで、おそらくその流れでこういったコメントの書き方になっているのでしょう。今のPHPを知っている人であれば、おおよそこういった書き方はしません。
PHPにはPHPDocというコメント形式が広く使われています。PHPDocはJavadocから派生したもので、多くの記法はそれによく似ています。PHPでコードを書くうえではこの形式が一般的なので、PHPDocの形式に従うことで、誰もが理解しやすいコメントを書くことができます。
たとえば、38~40行目にdo_add_item()
メソッドのコメントが次のように書かれています。
これをPHPDoc形式で書くと次のようになります。引数や戻り値についても記載があり、このメソッドのIN/OUTがわかりやすくなっています。
PHPDocを使う理由は、コードを読む人がわかりやすいということだけではありません。PHPには、リフレクションによるPHPDoc形式のコメントを抽出する機能があり、これを利用してメタデータの読み取りやアノテーションを実現しているツールがあります。たとえば、phpDocumentor では、PHPDoc形式のコメントからAPIリファレンスを自動生成することができます。また、EclipseやPhpStormといったIDEでは、補完候補やヒントの表示にPHPDoc形式のコメントを利用しています。
華麗なアスキーアートによるコメントも味があるのですが、PHPではPHPDoc形式によるコメントが広く使われているので、それに従うのが賢明でしょう。
4. 変数名がハンガリアン
次に気になるのは、変数名です。変数名の先頭には何やら「c」や「a_」「i」といったプレフィックス(接頭辞)が付いています。おそらくこれは変数の型を示すもので、「c」はcharacter型(string型?)、「a_」はarray型、「i」はinteger型を示すものだと推測します。こうした型に関する情報を変数名の前や後ろに付与して命名する記法をハンガリアン記法と呼びます。このハンガリアン記法は、以前のWindows系の開発でよく用いられていたもので、コードが書かれた年代を合わせて考えると、おそらくWindows系の開発をされていた方がこのコードを書いたのではないかと推察できます。
現在ではあまり見なくなったハンガリアン記法ですが、変数名を見ただけでデータ型がわかるという意味では、悪くないように思えます。ハンガリアン記法が良くない点というのはいくつかあるのですが、一番は「みんなが共通に理解しているルールが存在しない」ということが大きいように思います。おそらく前項にあるPHPDocのように標準化がされていれば、もしかするとこうして書くのが当たり前の世の中になっていたのかもしれないのですが、残念ながら今ではそうした記述は行われていません。
おそらくハンガリアン記法を知らない人にとっては、$a_Item
変数の「a_」が何を意味するのか、想像が付かないでしょう。これが$items
や$itemList
などであれば、item
が複数入っているリストのようなものであることが簡単に認識できます。つまり、変数の型を名前に含めるより、その変数が意味するところを明確に書いたほうがわかりやすいということですね。
ちなみに、30行目に登場する$CK_cCartId
変数は、いろいろ考えたのですが、わかりませんでした。「cCartId」の「c」は、characterだと思うのですが、その前の「CK_」は何でしょうね……[1]。うーん、といった具合に意味が取れないのであれば、ハンガリアン記法は役に立たないということです。
5. global文
30行目のコンストラクタで登場するのが悪名高きglobal
文です。これを見たら思わず身構えてしまうPHPerの諸兄も多いかと思います。
ご存じない方に向けて解説すると、global
文はグローバル変数をローカルスコープ内から操作できるようにする命令です。下記の例では、$CK_cCartId
というグローバル変数を操作できるようにしています。つまり、このコンストラクタ内では、グローバル変数$CK_cCartId
の参照も値の変更も可能になります。
グローバル変数の利用はできるだけ避けたほうがよいです。
その理由はいくつかあるのですが、まずグローバル変数は、影響範囲が大きいということです。名前のとおりグローバルな変数なので、PHPコードが動作している個所ではどこでも参照、変更が可能です。つまり、仮に値を変更した場合、予期せぬ個所にもその影響が波及してしまう可能性があります。この影響する個所を正確に把握しながらコードを書くというのは、なかなか大変な作業です。影響範囲を絞り、限られたスコープの中で変数の操作を行う方が、理解しやすく、安全なコードを書きやすくなります。
どうしてもグローバル変数を使わないと処理が書けない、もしくは使ったほうが遥かに効率が良いという場面があれば、その用途に限定して使うということはあるかもしれませんが、使う必要がないのに安易にグローバル変数を利用するのは避けるべきです。上記のコードはまさに悪い例で、単にコンストラクタの引数に$CK_cCartId
の値を引き渡すだけで用が済みます。これを実装した例が下記になります。$CK_cCartId
の値は、Cart
クラスを利用する側が生成しておき、インスタンス化する際に引数として渡すようにしています。
ほかには、カートIDの生成はコンストラクタで行い、Cart
クラスのメンバ変数$cCartId
にだけ、その値を入れておくという方法もあります。もし、Cart
クラスの外からカートIDを参照したい場合は、getCartId()
のようなゲッターメソッドで参照します。
global
文の利用は、ほかの代替手段で対応できる場合がほとんどです。今となってはglobal
文を利用しないといけない場面はほぼないといってよいでしょう。
6. 配列の操作
配列の操作においてもなかなか特徴があります。
まず、23行目では$a_Item
というメンバ変数を初期化しているのですが、下記のように空文字を代入して初期化しています。この行だけを見ると$a_Item
変数は、ハンガリアン記法による配列を想定しているのか、代入値である文字列を想定しているのかがわかりません。
そのあとのコードを見ると配列として操作しているので、ここではハンガリアン記法の勝利となるのですが、配列の初期化を行うなら、以下のように素直に[]
を渡したほうがよいです(ここではアクセス修飾子にprotected
を設定しています)。
次に114行目では、while文にて$this->a_Item
の値を順に参照しています。PHP 3当時はforeach
文が存在しなかったので、こういった配列の添字をインクリメントして各要素の値を参照する手法が多く取られていました。通常、配列を順に格納していけば添字は連続した値になるため、正常に動作するように思えます。ただ、もし添字に欠番があったり、添字は存在していても値がnull
やfalse
などの場合、while
文がその時点で終了してしまいます。
配列を順に参照していくのであれば、次のようにforeach
文を使うのがよいでしょう[2]。
なお、元のコードを実行すると、while
文の個所で最低でも1回はNotice
エラーが出ます。なぜNotice
エラーが出るのかは考えてみてください[3]。
治療すべきポイントを洗い出したところで、次回はこれらの問題点を治療していきます!