PHP Frameworks Day
PHP Frameworks Day は、昨年10月にウクライナのキエフで開催されたイベントです。様々なフレームワークに関する講演が行われました。
私がこのイベントを知ったのは、PHPDeveloper.orgで有名なChris Cornutt(別名enygma)がまとめているニュース配信、PHP Quick Fixのおかげです。Chrisに感謝します。
なぜすべてのPHPフレームワークはクソなのか
PHPの生みの親であるRasmus Lerdorf氏が、PHP Frameworks Dayカンファレンスに招待され講演を行いました。彼は主にPHPの最新開発状況について語りましたが、私にとって最も興味深かったのは質疑応答セクションでした。
その中で、誰かがRasmus氏にPHPフレームワークに対する意見を求めました。それに対する率直な質問に対し、Rasmus氏は率直に答えました(約31分47秒時点):「それら(PHPフレームワーク)はすべてクソだ!」
ゲストスピーカーがPHPフレームワークのカンファレンスに行き、それらすべてがクソだと言うのは奇妙に思えるかもしれません。しかし、聴衆はその答えを楽しんでいるようでした。いずれにせよ、Rasmus氏は彼が何を意味しているのかについて詳しく説明しました。
1. フレームワークは同じコードを不必要に繰り返し実行する
Rasmus氏は、汎用目的のフレームワークはすべて、万人のニーズに合わせて最適化されているわけではないと明らかにしました。
より具体的な不満は、フレームワークが提供するソリューションが、HTTPリクエストごとに不必要なPHPコードを繰り返し実行することにつながるという点です。Rasmus氏が挙げた例は、フレームワークはすべてのリクエストにおいて、アプリケーションが使用しているデータベースの種類をチェックし、対応するデータベースアクセス・クラスをロードしているというものです。アプリケーションがデプロイされた後、データベースの種類が変わることはないため、彼はこれを無駄だと考えています。
Rasmus氏の意見には同意しますが、この例はあまり説得力がないと思います。設定をチェックしてどのデータベースアクセス・クラスをロードするかを決定するのにかかる時間は非常にわずかだからです。例えば、データベースクエリの実行と比較すれば、クエリの実行は通常数ミリ秒、時には数秒かかることもあります。
この問題のより良い例は、フレームワークが実際の構成値をロードするために構成ファイルを読み込む必要がある場合です。
多くの場合、フレームワークはINIファイルから構成を読み込みます。PHPにはINIファイルをロードしてパースするための組み込み関数があります。1つの関数ですべてを実行できますが、INIファイルを読み込んでパースするには、パースされた構成値をチェックするよりも通常はるかに時間がかかります。
もしあなたのフレームワークが、YAMLやXMLのようにPHPが組み込みサポートしていない形式のファイルから構成値を読み込んでパースする場合、事態はさらに悪化します。フレームワークは純粋なPHPコードでパースを行わなければならないからです。これは、INIファイルをパースするPHPエンジンのCコードよりもはるかに遅いです。
より良い代替案は、構成値をPHPスクリプトファイルで定義することです。値を変数に代入するPHPスクリプトに構成値を記述するだけです。
PHPキャッシュ拡張機能を使用すると、PHPスクリプトは一度だけコンパイルされます。2回目以降の実行では、コンパイルされたオペコードがRAMからロードされます。これはファイルから構成をロードするよりもはるかに高速です。
2. フレームワークはあまりにも多くの相互依存クラスを要求する
Rasmus氏が言及したもう一つの点は、時にはフレームワークの特定のパーツだけが必要なのに、フレームワークのクラス間に依存関係が多すぎるため、単純な機能を使うだけでも多すぎるクラスをロードしなければならないという点です。
これがある程度事実であることは認めますが、特定のコンポーネント間の依存関係を減らそうとするフレームワーク開発者の努力も見られます。それでもなお、特定のニーズを持つアプリケーションにとっては何も付加価値を生まないような、多くのフレームワーククラス間での依存関係がしばしば存在します。
この問題に対処するために、オーバーヘッドを追加する不要なパーツを取り除くようフレームワークを修正する必要がある開発者もいます。これは、適応させたフレームワークの新しいバージョンにアップグレードするたびに同じ作業を行う必要があり、メンテナンスの悪夢を引き起こします。
Rasmus氏は、この問題を避けるために特定の目的に最適化されたフレームワークを使用することを提案しています。例えば、ブログを公開したいだけならWordpressやDrupalの使用を勧めています。
あるいはRasmus氏は、フレームワークが、開発者がアプリケーションごとに必要なコンポーネントの小さなサブセットだけを本番環境にプッシュできるようにすることを提案しています。
このソリューションはあまりにも一般的です。Rasmus氏は特定のフレームワークが物事を実装する方法まで踏み込んでおらず、なぜ特定のフレームワークがこれほど多くのコンポーネントを必要とするのかについてのコメントはありませんでした。
例えば、多くのフレームワークはランタイムORM(Object Relational Mapping)に依存しています。これらは、情報をレコードのテーブルとしてではなく、オブジェクトとして扱うことでデータベースへのクエリ方法を開発者が定義できるようにするコンポーネントです。
オブジェクト指向は、問題を抽象化し、解決策をオブジェクトのクラスにカプセル化するには良いものですが、特定のORMの動作方法はあまりにも多くの不要なオーバーヘッドを追加します。
開発者は、実行時に実際のクエリを作成するために、クラス変数(テーブルフィールド)、条件句、オブジェクト関係(テーブル結合)などを動的に指定するコードを書かなければなりません。実行時に変化する可能性のあるいくつかのパラメータ値を除いて、実行されるクエリはすべてのリクエストで同じであるため、これには多くのオーバーヘッドが追加されます。
このオーバーヘッドを回避するより良い解決策があります。実行時に動的にクエリを作成するのではなく、ORMクラス用のPHPコードを生成する別のツールを用意するだけです。生成されたクラスは、実行時にそれ以上のオーバーヘッドなしで実行するためのSQLクエリをすでにコンパイルしています。
私は2002年から、MetastorageというORMツールを開発した時からこのアプローチを使用しています。これは私が上記で説明したことを正確に実行します。オブジェクト、変数、関係、およびオブジェクトに適用する必要がある関数をプロジェクトファイルで定義します。
Metastorageは私のオブジェクト定義を処理し、クラスの関数を呼び出すだけで実行時に必要なクエリを実行するORMクラスを生成します。実行時にクエリ構築は行われません。
3. 不必要に複雑なソリューション
Rasmus氏が直接言及しなかったことの一つに、フレームワークが押し付けがちな複雑なソリューションがあります。
例えば、アプリケーションのバージョン移行がそのケースです。一部のフレームワークは、Ruby On Railsからマイグレーションの概念をコピーしました。これは、アプリケーションのバージョン間でデータベーススキーマを変更するためにコードを書かなければならないことを意味します。
これもMetastorageが開発者にとってより効率的で苦痛の少ない方法で対処していることです。Metastorageは、オブジェクト定義とは別のファイルにデータベーステーブルのスキーマ定義を生成します。最初にデータベーステーブルをインストールするインストール用クラスを生成します。
オブジェクト定義を変更した場合、インストール用クラスはデータベーステーブルにすでに挿入されているデータを破壊することなく、新しい定義でスキーマをアップグレードすることもできます。
これにより、開発が大幅に高速化され、アプリケーションのアップグレードでエラーが発生しにくくなります。このツールは常にデータベーススキーマをアップグレードするための正しいコードを生成するからです。マイグレーションコードを手書きする場合、修正に時間と労力を要するミスを犯す可能性があります。
4. Webサーバーの機能を複製している
Rasmus氏が直接言及しなかったもう一つの側面は、PHPコードがWebサーバーによってすでに完了している作業をやり直す必要があるという点に関連しています。
例えば、ルーティングとは、異なるURLパターンを持つリクエストを処理するために何らかのコード(コントローラー)を割り当てるプロセスです。多くのフレームワークは、アプリケーションに対してフロントコントローラーパターンの使用を推奨しています。フロントコントローラーはリクエストURLを分析し、実際にリクエストを処理するための特定のコントローラーをロードします。
ここで問題なのは、Webサーバーがすでにこれを行っているということです。リクエストURLを(mod_rewriteなどの設定に対して)マッチングさせ、適切なPHPスクリプトを実行することができます。
PHPにルーティングプロセスを処理させると、同じURLパターンを持つすべてのリクエストに対して同じタスクを実行するために、不必要なオーバーヘッドを追加することになります。これは、Rasmus氏の「同じ結果に到達するために同じコードを繰り返し実行するフレームワーク」という不満に当てはまります。
これは、PHPフレームワークがRuby On RailsやJavaから受けたもう一つの悪影響のようです。それらの言語では、Webサーバーがリクエストをアプリケーションサーバーに転送します。
PHPはWebサーバーと統合されて動作するため、このような仕組みである必要はありません。したがって、Webサーバーの機能を遅い方法で複製し、オーバーヘッドを増やすことに意味はありません。
その他の質問
同じカンファレンスで、Rasmus氏は他にも興味深い質問に答えており、それについてコメントする価値があると思います。
Zend Opcode CacheのためにAPCを廃止する
これはLately in PHPポッドキャストで何度も議論したトピックです。Rasmus氏は、PHPは新しいリリースが出るたびに最新のPHP開発を追従する1つのオペコードキャッシュを採用する必要があると説明しました。
オペコードキャッシュはいくつか存在します。Rasmus氏はAPCよりも成熟していて高速であるため、Zendのソリューションを採用するためにAPCを断念することに決めました。それにはZendが自社のソリューションをオープンソース化する必要がありました。
奇妙なことに、現在の公式PHPオペコードキャッシュのメンテナはDmitry Stogov氏です。彼はTurck MMCacheの元開発者であり、数年前にZendが自社のキャッシュ拡張機能の開発のために雇った人物です。
終わりよければすべてよし。PHPが公式のキャッシュ拡張機能を持つまでにこれほど時間がかかったのは残念です。公式拡張機能の欠如は、かつて他の言語を優遇していた多くのベンチマークでPHPを悪く見せていました。
PHPをバイナリコードにコンパイルする
誰かが、PHPをバイナリの形式にコンパイルしてコードを保護するソリューションはできるのかと尋ねました。
Rasmus氏は、PHPにそのようなソリューションが組み込まれることは決してないと明言しました。彼は、Zend(およびその他の企業)がそのためのソリューションを提供しているが、それらを破ることは比較的簡単であると正当化しました。そのため、彼はそのゲームに参加したくないと考えています。
これは事実ですが、Rasmus氏は単にPHPをオペコードにコンパイルして結果を暗号化するだけのソリューションを考慮しているに過ぎません。これはハッカーにとって破るのが決して難しくないソリューションです。
しかし、結果のコードをネイティブなアセンブリ機械語にコンパイルするというより良いソリューションがあります。機械語を逆コンパイルすることは常に可能ですが、作業を盗んだり、有益な方法で変更したりしようとする人々が理解できるほど実用的なPHPコードにリバースエンジニアリングすることは非常に困難です。
PHPコードのコピー防止ソリューションを探している多くの開発者が懸念していることの一つは、コードがインストールされているサーバーへのアクセス権を持つ誰かが、簡単にコードを変更してしまうことです。
顧客のために働く開発者が、顧客自身が開発者の知らない間にコードを変更してしまうという場面を何度も見てきました。これはメンテナンスの頭痛の種を生みます。時には、実際には彼らがコードを変更したにもかかわらず、コードがうまく機能していないと顧客が不平を言うこともあります。したがって、インストールされたコードを閲覧や変更しにくくするソリューションは役立つでしょう。
そのような場合、今日では開発者はPHARアーカイブを作成することでその問題を最小限に抑えることができます。これらは1つ以上のPHPスクリプトを含むバイナリアーカイブです。PHARアーカイブは本当の意味でのコピー防止ソリューションではありませんが、少なくとも開発者のコードをいじくり回そうとする顧客にとっては、いじることが難しくなるでしょう。
PHP変数の$記号
なぜ変数が$記号で始まるのかと尋ねられたとき、彼は文字列リテラルの中に変数を挿入できるようにするためだと説明しました。そのため、文字列の他の部分と変数を区別するためにマークが必要だったのです。
彼は文字列の内外で変数を同じ見た目にしたかったため、Perlが採用したソリューションに触発されて、変数の開始に$記号を選択しました。
Node.jsとノンブロッキングI/O
PHPがノンブロッキングI/Oプログラミングをサポートするのかと尋ねられたとき、Rasmus氏はlibevent拡張機能を使えばすでにそれが可能であると説明しました。しかし、そのようなプログラミングには、彼はGo言語でコードを書く方を好むでしょう。
いずれにせよ、残念ながらNode.jsなどで実行される非同期(ノンブロッキングI/O)プログラミングは、すべてをネストされたコールバックで処理する必要があるため、あまり快適ではありません。
コールバック内のネストされたコードは、コールバック関数の中にいるときにwhileループから抜け出せないといった非常にイライラする問題につながります。これはLately in JavaScriptポッドキャストで何度も議論したトピックです。
PHP 7のUnicodeとJIT
今後のPHPバージョンの計画について尋ねられた際、Rasmus氏はPHP 6のUnicodeサポート失敗から、それが野心的すぎた目標であることを学んだとコメントしました。そのため、彼はPHPがより小さなホップで進化することを期待しています。
彼が野心的すぎると考えているが、PHP 7で最終的に実装されるかもしれない2つの目標は、ICUよりもシンプルなアプローチに基づくUnicodeへのネイティブサポートと、おそらくGoogle V8やFacebook HHVMに基づくJITコンパイルエンジンです。
結論
Rasmus氏のインタビューは非常に興味深いものでした。なぜなら、特に汎用目的のフレームワークを使用している場合、理想とは言えないPHPでの物事の進め方について深く考えさせられるからです。
これらの意見に同意するかどうかにかかわらず、これらのトピックについてあなたがどう思うか、ここにコメントを投稿してください。