PHP を動かしてみよう
PHP をインストールしてあること。自分のコマンドラインでこういうのができるまでは自力でなんとかしましょう。
$ php -v
PHP 8.0.3 (cli) (built: Mar 4 2021 20:42:56) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.3, Copyright (c) Zend Technologies
with Zend OPcache v8.0.3, Copyright (c), by Zend Technologies
現在はもう、他に何のプログラミングもできないけど PHP を書いて FTP でアップロードできさえすれば仕事になる時代ではありません。Git が動くようにするとか NodeJS でフロントエンドやるとかと同じく、コマンドラインから必要なツールを使えるようにするのは、PHP エンジニアにとって普通に必須スキルです。
php コマンドが動いたら、こんなファイルを用意して...
<html><body>
<?php echo "Hello " . $_GET['name']; ?>
</body></html>
これでサーバーを立て...
php -S 0.0.0.0:8080
ブラウザから http://localhost:8080/index.php?name=World
にアクセスして Hello World が表示されるのを確認します。
PHP は echo
で生の文字を出力できます。特別なグローバル変数配列(PHP ではリストと辞書のデータ型は共通していて、配列という用語はこの共通データ型のことを言います) $_GET
で URL のクエリパラメータ部分を受け取ります。他に $_POST
や $_SERVER
といった変数も外部から入力される値を保持します。
動作を理解したら、このファイルは捨てましょう。このスタイルで PHP を書くのはこれっきりです。
伝統的なこの入門コードは XSS 脆弱性のパターンを踏んでいます。htmlspecialchars
関数でユーザー入力をエスケープし忘れないよう気をつけ続けなければなりません。プログラムが複雑になると、いつでもどこでもグローバル変数にアクセスできるのも困ります。
このまま高度化すると、URL の数だけ PHP スクリプトファイルができます。その共通部分を分割して include
などで取り込むことになります。名前の重複した定義をいかに避けるかがコツになってきます。
また、必要のない無駄な読み込みを最少化する苦労を背負い込みます。PHP のメリットは、HTTP リクエストごとにゼロから再評価しても実用性能になるところです。アプリケーションが育ったとき、実際には使わない無駄なコードをすべて再評価するとそのメリットが台無しです。
こういったスタイルの PHP 学習のほとんどは、残念ながら、現代的なフレームワークを使う開発スタイルにほとんど活かすことができません。これまでの PHP の歴史の中で、先人が発見したやってはいけないこと、やって辛かったので見捨てたことに興味があればご自由にですが、どう考えてもそこは後にするのが賢明です。
現代的な PHP と地続きに
まずコマンドラインで Composer を使えるようにします。Composer は PHP で主流のパッケージマネージャーであり、Java でいうクラスローダーでもあります。現在の PHP では、プログラムをクラス単位で管理するのが一般的です。
Composer を使って Slim を導入して使ってみましょう。
composer require slim/slim slim/psr7
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function(Request $request, Response $response) {
$response = $response->withHeader('Content-Type', 'text/plain');
$response->getBody()->write("Hello World");
return $response;
});
$app->run();
php -S 0.0.0.0:8080 -t public public/index.php
public/index.php
と、ひとつディレクトリを掘ったところに公開スクリプトを置くようにします。Composer を使うと、composer.json
と composer.lock
ファイルがプロジェクト直下にできます。また vendor
というライブラリを格納するディレクトリもできます。これらがアクセス可能になるのはおかしいので、Web に公開するディレクトリを限定するのです。
autoload.php
が Composer のクラスローダーです。__DIR__
はこのコードが記述されたファイルのディレクトリパスです。require
は指定したファイルがなかったらエラー停止する include
です。この autoload.php
によって、以後はクラス名を指定するだけで必要な PHP スクリプトがロードされるようになります。
use
はこの PHP スクリプト内でたんにクラス名だけを指定した場合に、どの名前空間のクラスにあたるかを定義する宣言です。複雑なライブラリには非常に多くのクラスがディレクトリ分けされ、またその配置にしたがって、それぞれがユニークになるよう名前づけされています。この長いフルネームを書いてもいいのですが、それではコードが横に長くなりすぎるので、use
を使って短い名前で簡単に使えるようにエイリアスしています。
$app =
以降がじっさいに動作する部分です。Slim では、というより現代的な PHP では、たったひとつの公開 PHP ファイルしかないのが通常です。URL ごとにいきなり動作するスクリプトを配置するのではなく、複数の「どんなとき何をするか」を何らかの方法でロジカルに定義しておき、これでさまざまなアクセスに応答するようにします。他の多くの言語も、Web アプリケーションを作る場合はこのような方法が一般的です。
->get('/', ...)
は「このアプリケーションは HTTP の GET メソッド (ブラウザでアドレスバーに URL を打ち込むなどの閲覧行為) でルートパスをリクエストされた場合」を意味します。そして、実際に何をするかを表すのが function (...) { }
で定義される関数です。HTTP メソッドとパスの組み合わせごとに、ユーザーのリクエストを入力としたレスポンスを返す関数を割り当てます。パラメーターに $response
がありますが、これはちょっと便利かなと Slim が与えてくれている参考用のレスポンス原型なだけで、やりようによっては無視することもできます。が、せっかくなのでここでは利用させてもらいます。
レスポンスヘッダに Content-Type
を指定したものを得たあと、そこにレスポンスのボディに文字列を書き込んで返しています。関数の入力と出力だけで HTTP 通信に必要なことがすべてまかなえました。グローバル変数やグローバルな状態管理はいっさい必要ありません。サンプルコードは、入力と無関係につねに同じ出力をする処理となっています。
ResponseInterface
と ServerRequestInterface
は Slim 固有のものではなく、 PSR-7
という業界標準で定められた型です。Python でいう WSGI、Java でいうサーブレット仕様と同様の、さまざまなベンダーが実装を提供できる共通の抽象です。
こうして挙動を定義したオブジェクトを $app->run()
することで、実際の応答動作が起きます。いきなり情報量が多いと感じるかもしれませんが、大部分は決まりきった記述なのでコピーでかまいません。どうせ最初は、プログラマーが独自の処理を行える余地なんて、->get()
の定義数を増やすことと、応答関数の内部だけですから。
JSON の API を作る
昨今の Web アプリケーションのサーバーサイド開発においては、HTML ページの表示よりも JSON の応答の方が簡単です。Content-Type=text/plain
だった Hello World の JSON 版を作ってみましょう。
$app->get('/', function (Request $request, Response $response) {
$response = $response->withHeader('Content-Type', 'application/json');
$response->getBody()->write(json_encode([
'message' => "Hello World",
]));
return $response;
});
$request
に応じて JSON の内容が変わるプログラムを書く必要がある場合どうするかは、すぐに想像できますね。ServerRequestInterface
を調べると、リクエスト情報へのアクセスが網羅されています。
HTML ページを作る(残念な方法)
JSON の文字列化は簡単なのですが、HTML は少々面倒です。この関数内で文字列処理をして HTML 全体を作るのは正気ではありません。文字列処理はテンプレートに、そしてそのリソースはロジックから分離するのが健全です。
$app->get('/', function (Request $request, Response $response) {
$response = $response->withHeader('Content-Type', 'text/html');
ob_start();
require __DIR__ . '/../resource/templates/index.html.php';
$body = ob_get_clean();
$response->getBody()->write($body);
return $response;
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1>Hello World</h1>
<?php /* Do something */ ?>
</body>
</html>
ob_start()
と ob_get_clean()
は生 PHP の出力コントロールです。HTML に組み込まれた PHP を使うと、評価と同時にいきなり動作して出力を始めてしまいます。なので出力をバッファリングして、バッファの内容を文字列として取得したあと、キャンセルします。これでレスポンスボディに使えるデータを得るトリックです。
しかしこれはいささか非現代的です。手続きにミスがある可能性を考えると、いつもこんな方法を取るのは安全とは言えません。せっかく避けていた include
文化を再度持ち込んでしまうのも本末転倒です。Composer を使っているのであれば、もっと便利なライブラリを使うのが賢いやり方でしょう。
PHP-DI
HTML レンダリングの前に、もう少しコードの効率を考えてみたいと思います。リクエストによらず常に同じレスポンスを返す関数にさえ、$request
引数が必要でしょうか。ライブラリ本来の用途というわけではありませんが、PHP-DI とその Slim ブリッジを使うと、この無駄を省くことができます。
composer require php-di/php-di php-di/slim-bridge
use DI\Bridge\Slim\Bridge;
use Psr\Http\Message\ResponseInterface as Response;
require __DIR__ . '/../vendor/autoload.php';
$app = Bridge::create();
$app->get('/', function (Response $response) {
$response = $response->withHeader('Content-Type', 'text/plain');
$response->getBody()->write("Hello World");
return $response;
});
$app->run();
AppFactory
の代わりに DI\Bridge\Slim\Bridge
を使うと、引数の $request
を省略することができます。
DI とは Dependency Injection (依存性注入) の略です。高度に責務分担されたソフトウェアは、じっさいにやりたい処理内容よりも、あれをやるにはこれが必要、あれをさせるためにこれを渡して、といった、部品の構成の面倒の方が増すものです。Composer によって大量のサードパーティライブラリの力を借りられるようになった PHP は、それまでの「やりたいことだけ書けばよい」でいつか複雑さの限界を迎える世界と一線を画す進歩を遂げました。と同時に、高度なソフトウェアとしての構成管理の問題も発生しました。
DI の考え方では、やりたいことに注目しているときは、そういった構成の悩みをいっさい捨てて、「きっと他の誰かに与えてもらえる」と割り切って、その詳細を気にせず完結させてしまう方針を取ります。依存は別の誰かが注入するのです。もちろん、その注入を自分で書いた別のプログラムがやってもいいのですが、簡単な定義と推論の自動化によって埋めてくれる装置があれば利用するのが賢い判断です。そのような装置を DI コンテナと言います。PHP-DI は PSR-11
標準に準拠した DI コンテナです。
PHP-DI Slim Bridge によって、「引数の順序を守った関数コール」でできていた部分が、「関数が必要とするものをいい感じに与えてくれる」かたちになったのです。構成ロジックどころか定義すらなくても推測で自動注入されるのを、オートワイヤリングと呼びます。PHP-DI はデフォルトでオートワイヤリングが効いています。
ところで、必要なものがオートワイヤリングされるということは、もし関数の引数に「便利なテンプレートエンジンが必要なんだけど」と書かれていれば...
HTML ページを作る(スマートな方法)
PHP で動く便利なテンプレートエンジンのひとつに、Twig があります。Composer で追加し、利用できるかたちにしてみましょう。
composer require twig/twig
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
use Psr\Http\Message\ResponseInterface as Response;
use Twig\Environment as Twig;
use Twig\Loader\FilesystemLoader;
require __DIR__ . '/../vendor/autoload.php';
$builder = new ContainerBuilder();
$builder->addDefinitions([
Twig::class => function () {
$loader = new FilesystemLoader(__DIR__ . '/../resource/templates');
return new Twig($loader, [
'cache' => __DIR__ . '/../var/cache/twig',
]);
},
]);
$app = Bridge::create($builder->build());
$app->get('/', function (Response $response, Twig $twig) {
$response = $response->withHeader('Content-Type', 'text/html');
$body = $twig->render('index.html.twig');
$response->getBody()->write($body);
return $response;
});
$app->run();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
注目するのは function (Response $response, Twig $twig)
の部分です。Twig が与えられることを完全にあてにしています。あとは動く状態のはずな Twig さえもらえれば、->render()
だけで HTML レンダリングはおしまいです。
その Twig の本体は、ContainerBuilder
の addDefinitions()
で定義されています。Twig::class
が必要とされたときもし DI コンテナ内にまだそのオブジェクトがなければ、次のような関数で生成すること、と読みます。
こうしてユーザー定義依存の入った DI コンテナを構築して $app
に与えてやると、リクエストのハンドリングで Twig が必要になったなら、定義済みの生成方法で適切なオブジェクトを注入してくれるというわけです。
コードを整理する
PHP-DI Slim Bridge にはもうひとつ、リクエストのハンドリング関数さえ必要に応じて生成するという素敵な機能があります。Composer の autoload.php
がユーザークラスをロードできるようになっていれば、$app->get()
の関数を [クラス名, メソッド名]
の2要素配列に置き換え可能です。
use App\Controller\HomeController;
$app->get('/', [HomeController::class, 'index']);
そしてもちろんこの HomeController
の生成のさいにも、オートワイヤリングによる注入は機能します。
まずはユーザークラスを扱えるように composer.json
に追記します。
{
"require": {
".../...": "^x.x"
},
"autoload": {
"psr-4": {
"App\\": "./src"
}
}
}
この追記をしたあと、composer install
を叩いて autoload.php
を更新します。これで、App\
で始まるクラスは src
ディレクトリから自動ロード可能になりました。
<?php
namespace App\Controller;
use Psr\Http\Message\ResponseInterface as Response;
use Twig\Environment as Twig;
class HomeController
{
private Twig $twig;
public function __construct(Twig $twig)
{
$this->twig = $twig;
}
public function index(Response $response): Response
{
$response = $response->withHeader('Content-Type', 'text/html');
$body = $this->twig->render('index.html.twig');
$response->getBody()->write($body);
return $response;
}
}
index.php
にあった関数を HomeController::index
メソッドに移動させています。ただし、Twig は引数ではなく $this
のフィールドにしてあります。このフィールドは、コンストラクタ __construct()
で初期化しています。PHP-DI がコンストラクタの引数に適当なオブジェクトを与えてくれるのを期待しています。PHP-DI のオートワイヤリング、もともとはこの「コンストラクタに適当な値を注入する」機能だけです。Slim Bridge は実はかなりサービス旺盛です。
ついでに、依存の定義部分を public/index.php
から config/config.php
に移動させておきましょう。今後より多くのオブジェクトを定義していくと、行数が膨れ上がりそうなので。
$builder = new ContainerBuilder();
$builder->addDefinitions(require __DIR__ . '/../config/config.php');
// なんと require/include は return できる!!
<?php
use Twig\Environment as Twig;
use Twig\Loader\FilesystemLoader;
return [
Twig::class => function () {
$loader = new FilesystemLoader(__DIR__ . '/../resource/templates');
return new Twig($loader, [
'cache' => __DIR__ . '/../var/cache/twig',
]);
},
];
結果、index.php
はこうなります。
use App\Controller\HomeController;
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
require __DIR__ . '/../vendor/autoload.php';
$builder = new ContainerBuilder();
$builder->addDefinitions(require __DIR__ . '/../config/config.php');
$app = Bridge::create($builder->build());
$app->get('/', [HomeController::class, 'index']);
$app->run();
こうして整理できたディレクトリ構成はこのようになります。
.
├── composer.json
├── composer.lock
├── config
│ └── config.php
├── public
│ └── index.php
├── resource
│ └── templates
│ └── index.html.twig
├── src
│ └── Controller
│ └── HomeController.php
└── var
└── cache
└── twig
今後 URL のパターンが増えてくれば $app->get();
を config/routes.php
などに分けてもよいかもしれません。
ミドルウェア
自由に編集していると、エラーが起きたとき、適切な HTTP ステータスコードにならなかったり、やたらと読みにくいメッセージが出たりするのが気になることはあります。->run()
の前にこうするとエラー時の表示がいい感じになります。
$app->addErrorMiddleware(true, true, true);
$app->run();
自分がハンドラを定義したパスだけでなく、HTTPハンドリング全体に関わる問題を扱う機構として、PHP-15
で標準化されたもののひとつに、ミドルウェアがあります。
TBD: PHPUnit + bootstrap.php, PSR-17, .env, log, docker-compose, DBAL