##動機
Google App EngineでPHP7のサンプル的なプロジェクトを動かそうとしてえらく苦労したのでその顛末をここに記す。
構成としてはルートに複数のPHPファイルと背景画像、その下のフォルダにPHPファイルやhtmlファイル、画像などがあるようなものだ。HerokuやAWS ElasticBeanstalk、Azureではローカルで動いていたPHPのプロジェクトをそのままデプロイすればそのまま動いた、実に簡単だった。しかしGoogle App Engineではそうはいかなかった。
##app.yaml
まず、アプリの設定情報を記述するapp.yamlを書かないといけない。どのバージョンを使うのかとか、アクセスされたパス、拡張子に応じてどのような動作するのかとか。いちいち言わんでもわかるやろと思ってしまうが。
で、app.yaml、phpという単語でググるとこのPHP5用のページがトップに表示されるのだが、PHP5とPHP7とでは動作が異なるので、PHP7のときはこれは使えない。上の方に「PHP7の使用を強くお勧めする」と書いてあるが、字が小さいので気が付かなかった。
PHP7のときはどんなふうに書けばいいのかというとこちら。
違いは拡張子phpの場合で、PHP5のときはphpのファイルをスクリプトとして処理せよという形になっているが、PHP7のときは拡張子の条件は消え、全てのファイル(.*)に対し、autoになっている。
# Serve php scripts.
- url: /(.+\.php)$
script: \1
# Serve your app through a front controller at index.php or public/index.php.
- url: .*
script: auto
コメントには「index.phpのフロントコントローラを介してappを提供しろ」と書いてある。
はてどういうことだろうと思いながら、とりあえず動かそうとすると、
ルートに置いたindex.phpのスクリプトは正常に動作するが、同じ階層にある別のphpファイルや下の階層のphpファイルにアクセスしようとしても、index.phpにリダイレクトされる。なんでや。
##フロントコントローラ
いろいろ調べてみるとどうやらindex.phpを入り口(フロント)として、そこから他のファイルへのアクセスを振り分けてやる(コントロールする)のがお作法らしいということがわかる。元々あったindex.phpはそのままにしておきたいので、ここではcontroller.phpという名前で新たにファイルを作り、そこを入り口にしてやる。入り口の名前はapp.yamlにentrypointとして書けば良い。
entrypoint: serve controller.php
そしてcontroller.phpはどんなふうに書いたらいいのか。いろいろさまよってここにたどり着いた。
なんだここに大事なことが大体書いてあるじゃないか。Googleのマニュアルは広大すぎてなかなか必要なところにたどり着けない。
そしてサンプルコードがGitHubに上がっている。
https://github.com/GoogleCloudPlatform/php-docs-samples/tree/master/appengine/php72/front-controller
ここでは
- Slim Frameworkを使用する場合
- WordPressを使用する場合
- 正規表現でやる場合
の3パターンを上げてくれてある。迷わず正規表現の例を見る。
正規表現は何回やっても覚えられないので、調べながら格闘、このサンプルコードがやっているのは
spanner.php、monitoring.php、speech.phpにアクセスしてきた時はそのファイルを応答として返すということのようだと理解する。
// Static list provides security against URL injection by default.
$routes = [
'spanner',
'monitoring',
'speech',
];
// Keeping things fast for a small number of routes.
$regex = '/\/(' . join($routes, '|') . ')\.php/';
if (preg_match($regex, $_SERVER['REQUEST_URI'], $matches)) {
$file_path = __DIR__ . $matches[0];
if (file_exists($file_path)) {
require($file_path);
return;
}
}
とりあえず拡張子phpのファイルは全部同じように処理して欲しいので、
正規表現の式を以下のように書き換えた。
先頭の「//」は意味がわからなかったので/にした。
$regex = '/.+\.php/';
if (preg_match($regex, $_SERVER['REQUEST_URI'], $matches)) {
$file_path = __DIR__ . $matches[0];
if (file_exists($file_path)) {
require($file_path);
return;
}
}
##app.yaml再び
これでとりあえずphpのアプリはみんな動いた。だが普通のhtmlファイルが表示されない。
拡張子がhtmlのときの動作がapp.yamlで定義がされていないからである。
再びapp.yamlを修正。
Googleのマニュアルで例として書かれていたものは、stylesheet以下のファイルと拡張子gif
、png、jpgの場合は静的リソースとしてそのまま配信しろ、それ以外はscript: autoという内容なので、htmlはscript: autoで処理されてしまうようである。何故だ。
handlers:
# Serve a directory as a static resource.
- url: /stylesheets
static_dir: stylesheets
# Serve images as static resources.
- url: /(.+\.(gif|png|jpg))$
static_files: \1
upload: .+\.(gif|png|jpg)$
# Serve your app through a front controller at index.php or public/index.php.
- url: .*
script: auto
やりたいのは拡張子がphpの時だけスクリプトとして処理して、それ以外は全部そのまま配信して欲しいので、以下のようにした。上に書いてあるものが優先的に適用されるようである。
handlers:
- url: /.+\.php
script: auto
- url: /(.+)$
static_files: \1
upload: .+$
.*(任意の0文字以上)ではなく.+(任意の1文字以上)にしたのはルートにアクセスされた際にindex.phpに飛んで欲しいから。.*だとルートにアクセスされた際もそのまま静的ファイルで応答しようとしてエラーになった。
##結論
controller.phpの方にもルートにアクセスされた場合にindex.phpに飛ばす処理を追加し、最終的にapp.yaml、controller.phpは以下の形になった。これでやっと思い通りの動きをするようになった。
こんな簡単な記述でいいのに、ここまでたどり着くのに3日かかった。Googleのマニュアル難しすぎ。なんでみんなあれで理解できるんだろう。
runtime: php73
entrypoint: serve controller.php
handlers:
- url: /.+\.php
script: auto
- url: /(.+)$
static_files: \1
upload: .+$
if($_SERVER['REQUEST_URI'] == '/'){
require('index.php');
return;
}
$regex = '/.+\.php/';
if (preg_match($regex, $_SERVER['REQUEST_URI'], $matches)) {
$file_path = __DIR__ . $matches[0];
if (file_exists($file_path)) {
require($file_path);
return;
}
}
http_response_code(404);
exit('Not Found');