LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

PHPエンジニア入門ドラフト

Last updated at Posted at 2021-03-28

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 コマンドが動いたら、こんなファイルを用意して...

index.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
public/index.php
<?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.jsoncomposer.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 通信に必要なことがすべてまかなえました。グローバル変数やグローバルな状態管理はいっさい必要ありません。サンプルコードは、入力と無関係につねに同じ出力をする処理となっています。

ResponseInterfaceServerRequestInterface は Slim 固有のものではなく、 PSR-7 という業界標準で定められた型です。Python でいう WSGI、Java でいうサーブレット仕様と同様の、さまざまなベンダーが実装を提供できる共通の抽象です。

こうして挙動を定義したオブジェクトを $app->run() することで、実際の応答動作が起きます。いきなり情報量が多いと感じるかもしれませんが、大部分は決まりきった記述なのでコピーでかまいません。どうせ最初は、プログラマーが独自の処理を行える余地なんて、->get() の定義数を増やすことと、応答関数の内部だけですから。

JSON の API を作る

昨今の Web アプリケーションのサーバーサイド開発においては、HTML ページの表示よりも JSON の応答の方が簡単です。Content-Type=text/plain だった Hello World の JSON 版を作ってみましょう。

public/index.php
$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 全体を作るのは正気ではありません。文字列処理はテンプレートに、そしてそのリソースはロジックから分離するのが健全です。

public/index.php
$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;
});
resource/templates/index.html.php
<!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
public/index.php
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
public/index.php
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();
resource/templates/index.html.twig
<!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 の本体は、ContainerBuilderaddDefinitions() で定義されています。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 に追記します。

composer.json
{
    "require": {
        ".../...": "^x.x"
    },
    "autoload": {
        "psr-4": {
            "App\\": "./src"
        }
    }
}

この追記をしたあと、composer install を叩いて autoload.php を更新します。これで、App\ で始まるクラスは src ディレクトリから自動ロード可能になりました。

src/Controller/HomeController.php
<?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 に移動させておきましょう。今後より多くのオブジェクトを定義していくと、行数が膨れ上がりそうなので。

public/index.php
$builder = new ContainerBuilder();
$builder->addDefinitions(require __DIR__ . '/../config/config.php');
// なんと require/include は return できる!!
config/config.php
<?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 はこうなります。

public/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

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up