読者です 読者をやめる 読者になる 読者になる

MANA-DOT

PIXEL ART, PROGRAMING, ETC.

PHP Badparts 1.require文

PHP bad parts PHP プログラミング

ScreenClip

PHPだ。業務で使っていてたしかにそう思う。同じく業務でScalaを書いている時のほうが2倍効率よくコードが書けていると思うし、3倍安心できるし、5倍楽しい(当人比)。

しかし、PHPだ、だとバカの一つ覚えのように言うのではなんの生産性もない。説得力もない。小学生のいじめと同レベルかそれ以下だ。 私は小学生のままで入れるならそうしたいものであるが、そうではなくもう立派な大人であるからして、このPHPだという気持ちを生産的なことに繋げたいと思う。

つまりであるからして、当ブログの「PHP bad parts」と称したカテゴリに、PHPの悪いところ、悪い点、なぜ悪いのか、どうすると悪いのか、そういったことをまとめ、出来れば具体的な対策を書いていこうと思う。そうすることにより、私個人や、読者様にとっても次のようなメリットが有ると考えている。

  • PHPの悪いところを整理できる
    • 私自身、PHPは悪いと使っていて確かに感じるが、いざ人に何が悪いのか聞かれると答えられないことが多い。使っていて感じたことをここにまとめていき、今後そういう場面でとっさにdisれるようにしたい。つまりは備忘録である。
  • PHP使用の抑止力になる
    • ここにまとめた糞な点が、新しいプロジェクトでのPHP採用を未然に防ぐ結果になるかもしれない。悪い点をブログというメディアを通して少しでも共有することで、より多くの人に悪い点を知って貰いたい。
  • 不運なことに、PHPを使わなければならないときに、注意すべき点を予見できる
    • 実際大怪我する前に、怪我をする可能性のある箇所を知れるのは大きなアドバンテージである。実際にPHPで書かれた業務システムでは、そういう大怪我は個人だけではなく、会社のひいてはユーザーの怪我となる。

私はすべてのプログラマの幸せを願う。このカテゴリにまとめていく駄文が、そのための糧となれば幸いである。

require文とは

というわけで前置きが長くなってしまいましたが、第一回の題材はrequire文です。 少しでも大きなプログラムを記述するなら必須であろうrequire文、 Javaではimport文、Cでは#includeマクロが相当します。 もしこれを使っていない巨大なシステムがあろうものなら、おそらくそれは人間の手に負えるものではないでしょう。 この構文の使用目的は言わずもがな、ファイルの分割による整理・役割分担と、複数ファイルからの同一ロジックの参照による再利用性の向上、 ひっくるめて言うとモジュール化のための構文です。 同種の構文としてinclude, include_once, require_onceがありますが、多くのプロジェクトで利用するのはrequire文の亜種であるrequire_once文でしょう。

例えば以下のように使いますね。

require 'foo.class.php';
$foo = new Foo();

この例では、foo.class.phpという別のファイルに定義されたFooというクラスを利用するためにrequireしています。 Fooというクラスには汎用的な処理を記述されているのか、はたまたこのファイル中に書き切るにはいささか巨大な処理が記述されているのか、ともかく適切にモジュール化されたプログラムは、いろいろな面で有用です。

このrequire文についていくつかの問題を指摘したいと思います。

構造を持っていない

require文の扱う引数は構造を持っていません。どういう話かというと、

require 'hoge.php';

という構文のうち、'hoge.php'には任意のPHPの文字列型を返す式をとることができます。 つまり、以下の記述は全て有効です。

require __DIR__.'/hoge.php';
require LIB_DIR.'/hoge.php';
require MyClass::getPath('/hoge.php');

このことは一見便利そうですが、大きなプログラムを書く場合に枷となります。

例えば、上記のLIB_DIR、MyClassなどの値が存在していない場合、プログラムはエラーとなってしまいます。 これは、requireする側にまずLIB_DIRやMyClassなどの値が存在する状況である責任を追わせることになります。 そして、そういう責任はシステムが巨大になればなるほど、破られやすくなります。 大規模なシステムを作る際に悲劇が起こりにくいのは、各プログラマ責任を追わせにくい プログラム言語/フレームワーク/ライブラリ なのかなと個人的には思っています。

また、もう一つの弊害として、そもそも与えられている文字列が有効かどうかわからないという点があります。 大きなプログラムで、往々にしてrequire_onceでそもそも存在していないファイルを読み出し、プログラムの実行前に終わっていた、という事を見かけてきました。 これはたいてい、深いモジュールから浅い階層の共通ファイルを見に行こうとする際にディレクトリの数を間違えていたり、 そういうのを吸収するための上記のような定数や関数に何らかのバグが潜んでいる場合に起こります。

このことは例えばJavaのimport文では比較的うまく解決されています。 import文で扱うのはそもそもファイルパスの文字列ではなく、パッケージという構造化された概念です。

package net.manaten;
import java.util.ArrayList;
...

package文によってプログラムから自分の厳密な居場所を宣言し、import文はそうして作らてたパッケージ階層によってクラスをインポートします。そして、import文に書かれたパッケージの有用性は、実行前に、コンパイルの時点で明確にわかります。 これはPHPスクリプト言語であることとも関わりますが、少なくともこの点においてはJavaが優秀であるといえます。

動的である

前述に話題と似ていますが、値が動的であることです。 完全な即値としてrequireしている場合は良いのですが、 たいていは利便性のために定数や関数を挟んでrequireします。

require '/hoge.php'; // これは即値

require LIB_DIR.'/hoge.php'; // 定数
require MyClass::getPath('/hoge.php'); // 関数

これは、require先が直感的には(つまり人間には)わかるかもしれないが、実際のところは実行するまでわからないということです。 先ほどのrequireで存在しないファイルを読み込んでしまう話に通じています。

また、同じrequire文であっても、同じファイルをrequireするかどうかは本質的にはわからないということでもあります。

静的に読み込まれるファイルが判断できないということは、外部プログラムによる自動解析にも不都合で、 簡単な例ではIDEによる完全な保管が難しくなります。

include_path に引きづられる

require 'hoge.php';

という即値のrequireだからといって安心してはいけません。 hoge.php は、include_pathというphp環境変数によってその場所が定まります。

もっと厄介なことに、include_pathに複数のパスが記述されていて、そのどちらにもhoge.phpがある場合はどちらが呼び出されるかもはやわかりません。おそらく最初に記述されている方が呼び出されるのですが、明らかにバグの原因になるでしょう。

スコープを引き継ぐ

これはあまり知られていない上に、使われてもいないと思いますが、もし使われていると最悪な性質です。 PHPマニュアルによると、

"ファイルが読み込まれるとそのファイルに含まれるコードは、 includeもしくは requireが実行された 行の変数スコープを継承します。 呼び出し側の行で利用可能である全ての変数は、読み込まれたファイル内で利用可能です。 しかし、読み込まれたファイル内で定義されている関数やクラスはすべて グローバルスコープとなります。"

と書かれています。

すなわち、

// fuga.php
echo $this->var;

というファイルが有るとき、

// hoge.php
class Hoge {
  private $var = "I'm hoge!";
  public function run() {
    require 'fuga.php';
  }
}
$hoge = new Hoge();
$hoge->run();

というコードは動作するのです。そんなバカな。 しかもたちの悪いことにprivateな変数を参照することができるのです。 そして一貫性のないことに、「関数やクラスはすべて グローバルスコープとなります」という仕様。 この仕様に依存したコードはそもそも書いてはいけないが、実際に見かけたことがあるので注意すべきです。

対策

踏まえて、いくつかの対策を考えてみました。

requireはファイル先頭でしかしない。

スコープを引き継ぐ という問題の簡単な解決法です。 また、先頭ですることでもしもパスが無効な場合、すぐにプログラムが終了するためすぐに気づくことができます。

フルパスで指定する

include_path の問題の回避となります。

require用のシステム全体で共通の関数を用意する。その関数はしっかりテストを書く

requireをラップした共通関数を用意してあげます。 更に、その関数をしっかりテストしてやることで、 Javaでは実行前にわかるパスの不備などを補ってやります。 ラップした関数では関数内でrequireすることになるので、スコープの問題には注意です。

もしくは、パスを返す関数群を定義してやり、requireするときは必ずその関数を使うようにするというルールをチーム内で設けるのも良いでしょう。

オートローダを使う

PHPにはオートローダという機能があるためそれを利用します。 オートローダを利用することで、プログラマはrequire文を書く必要がなくなり、上記の危険性からは開放されます。 同じくオートローダはしっかりとテストしてやる必要があります。 個人的には明示的にファイルの先頭でrequireしたほうが、ファイルの見通しが良くなるため好みです。

網羅的なテストを行う

すべてのプログラムのすべての挙動に対してテストがしてあれば、すべてのrequireも正しく動いているということであり、 心配することは何もありません。 これが可能なプロジェクトは多くはないと思いますが、もし可能なら最強の対策であると思います。

まとめ

大きなプログラムを書く場合に必須である構文、require文。 そのrequire文にはたくさんの落とし穴があることを挙げました。 いくつか対策を挙げましたが、個人的にはしっかりとテストされたrequire用の関数を作り、 それをファイル先頭でのみ利用するという運用が良いと思います。 オートローダは一見便利ですが、利用しているクラスの所在がひと目でわからなくなるのがマイナスです。

ですがぶっちゃけ外部モジュール読み込みくらいでそこまでしてようやく安心できるのなら、言語の機能内実行前に不備のわかるJavaを使うほうがマシというのが個人的な意見です。