LoginSignup
11
14

More than 5 years have passed since last update.

PHP構築メモ: ライブラリのデモ用PHPアプリ

Last updated at Posted at 2017-06-15

最近どんどんPHPの書きかたがわからなくなっていくので、自分の設計したコードをメモしていく。

フレームワークを使ったPHPアプリケーションの構成技法については比較的に知識が共有されやすいが、フレームワークに依存しないPHPの構成についてはまとまった資料がないように感じられるためユースケースを共有する。

いままで書いた記事

諸元

これは何? Mastodon API Client/SDK for PHPの動作検証(と、概念実証)をするための簡単なPHPアプリケーション。
想定利用者 PHP開発者 (Mastodon API/SDK利用者)
PHPバージョン PHP 5.5+, 7+, HHVM
プラットフォーム 特に規定しない (UNIX系OS+ビルトインウェブサーバー)を想定

構成

ソースコードのリポジトリがSDKライブラリと同じものを共有するので、sample/以下に配置。ただしライブラリの利用者がプロダクションにインストールするためには不要なファイルなので.gitattributeに設定することで、パッケージには含まれないようにした。composer.jsonについてはSDK本体と分割することも可能だったが、開発用アプリケーションなので構成の簡便のためにrequire-devに含めた。

ディレクトリ

sample/
├── README.md
├── cache
│   ├── pawoo.net.json
│   ├── qiitadon.com.json
│   └── session
│       └── sess_c599lo6g9jpnl2p6rkj4ckirv9
├── inc
│   ├── app.php
│   ├── bootstrap.php
│   ├── functions.php
│   ├── routes.php
│   └── variables.php
├── public
│   ├── favicon.ico
│   ├── index.php
│   └── robots.txt
└── view
    ├── 404.tpl.php
    ├── _login.tpl.php
    ├── acct.tpl.php
    ├── body.tpl.php
    └── index.tpl.php

5 directories, 18 files

inc/

ここには5つのファイルがある。

ファイル名 概要
functions.php Webサービスとしての汎用的なユーティリティ函数を定義
apps.php アプリケーションのための関数
routes.php ルーティングごとの処理をクロージャで定義
variables.php データをつっこむ謎コンテナ(謎)
bootstrap.php 上記のファイルとかComposerのオートローダーをまとめて読み込んで、実行の前処理をする

functions.phpapps.phpの区別はいくらか曖昧に感じられるが、アプリケーションのドメインに関るもの(ここでは、Mastodonのための処理)app.phpに、それ以外のWebアプリケーションのための汎用処理(別の目的のアプリケーションに移植しても利用できる処理)に関してはfunctions.phpに記述するものとして峻別した。

routes.phpはルーティング定義を記述する。詳細はシンプルなルーティングがしたかったを読んでほしい。routes.phpを分割するアイディアはSlimから拝借した

variables.phpはグローバル変数定義… などではなく、変数をまとめてぶちこんでおくvariableクラスを定義する。PSR-1 基本コーディング規約(日本語)に則るならばクラス名はCamelCaseで書くべきなのだけれども、ここはオレオレの世界なので完全に無視した。

せっかくなので名前空間についても言及しておくと、app.phpで定義される関数はapp\名前空間に属し、それ以外は基本的にトップレベル名前空間を利用した。

view/

HTMLテンプレートにはPHPをそのまま利用した。スクリプト処理を記述する(HTMLを含まない)PHPファイルと区別するため、拡張子は.tpl.phpとした。

PHPなどの継承概念を持たないテンプレートエンジンでページテンプレートを共通化するのは、以下のようなパターンがあるだろう。

  • A. 全ページのテンプレートからヘッダとフッタをincludeする
  • B. 共通ページテンプレートから固有のテンプレートをincludeする

これをコードにすると以下のようになる。

A-page.tpl.php
<?php include isset($header_tpl) ? $header_tpl :  __DIR__ . '/header.php' ?>
<main>
    <!-- それぞれのページのメインコンテンツ -->
</main>
<?php include isset($header_tpl) ? $header_tpl :  __DIR__ . '/header.php' ?>
B-body.tpl.php
<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title><?php h(isset($title) ? $title : 'デフォルト') ?></title>
</head>
<body>
    <!-- 共通ヘッダ -->
<main>
    <!-- 固有のメインコンテンツのテンプレートを読み込む -->
    <?php isset($main_tpl) && include $main_tpl; ?>
</main>
<footer>
    <!-- 共通フッタ -->
</footer>
</html>

このABのパターンは「それぞれのテンプレートから共通部分を読み込む」「共通テンプレートから固有部分を読み込む」と、includeの方向が反対であることを、おわかりいただけるだろうか。

特に大きな理由もあるかと言ったらないのだけれど、今回はBを採用することにした。サンプル用の小さなWebアプリケーションなので、例外的なヘッダやフッタのページもないため、こちらの方が利用しやすかった。

いくつかのページから読み込まれる可能性がある部分テンプレートは_hoge.tpl.phpのように先頭に_を付けることで差別化した。

固有ページテンプレート

<?php
/**
 * Template for index
 *
 * @author    USAMI Kenta <tadsan@zonu.me>
 * @copyright 2017 Baguette HQ
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0
 */
/** @var $var variables */
?>
<h1>Mastodon Sample App</h1>

<ul>
    <?php foreach ($_SESSION['mastodons'] as $acct => $mastodon): ?>
        <li>
            <a href="<?= h(router()->makePath('acct', ['acct' => $acct])) ?>">
                <?= h($acct) ?>
            </a>
        </li>
    <?php endforeach; ?>
</ul>

<?php include __DIR__ . '/_login.tpl.php'; ?>

初期化

mastodon-api/bootstrap.phpにある。

bootstrap.php
call_user_func(function() {
    // 新規開発プロジェクトなのでエラーレベルは最初から最高にする
    error_reporting(-1);

    // 開発環境ではWhoopsを有効化
    if (!is_production()) {
        $whoops = new \Whoops\Run;
        $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
        $whoops->register();
    }

    // 設定情報を読み出す
    $dotenv = new Dotenv\Dotenv(dirname(__DIR__));
    $dotenv->load();
    $dotenv->required('MY_PHP_ENV');
    $dotenv->required('SERVICE_BASE_URL');

    // セッション設定
    session_save_path(realpath(__DIR__ . '/../cache/session/'));
    session_start();

    // セッション値の地ならし
    app\gc_session();
});

filp/whoopsを有効化してる程度で、特別なことはしてない。今回は雑に開発環境でのみ有効化することにしたが、本番環境でも利用する方法があるが、ここでは割愛する。 WEB+DB PRESS Vol.96には書いたし。

エントリーポイント

mastodon-api/index.phpです。せっかくなので、略さずに書く。

<?php

/**
 * Mastodon SampleApp core application file
 *
 * @author    USAMI Kenta <tadsan@zonu.me>
 * @copyright 2017 Baguette HQ
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0
 */

require __DIR__ . '/../inc/bootstrap.php';
// ↑ __DIR__ をくっつけて、絶対ディレクトリがずれないようにします

// ここからはビルトインサーバー用
// http://php.net/manual/ja/features.commandline.webserver.php
if (php_sapi_name() === 'cli-server') {
    // ディレクトリトラバーサルの余地がないよう(念には念を入れて) .. が入ってたら糸冬了
    if (strpos($_SERVER['REQUEST_URI'], '..') !== false) {
        http_response_code(404);
        return true;
    }

    // ファイルが実在するなら、後のことはビルトインサーバーに任せた
    $path = __DIR__ . implode(DIRECTORY_SEPARATOR, explode('/', $_SERVER['REQUEST_URI']));
    if (is_file($path)) {
        return false;
    }
}

// ルーティング定義を読み込む
$routes = require(__DIR__ . '/../inc/routes.php');

router($router = new \Teto\Routing\Router($routes));

$action = $router->match($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

/**
 * @var int   $status  HTTPステータス
 * @var array $headers HTTPヘッダの連想配列
 * @var string|false $content
 */
list($status, $headers, $content) = call_user_func($action->value, $action);

http_response_code($status);
foreach ($headers as $name => $header) {
    // ヘッダーインジェクション対策はPHP側でやってくれるから、ここでは何もしないよ
    // http://php.net/manual/ja/function.header.php
    header("{$name}:{$header}");
}

if ($content !== null) {
    echo (string)$content;
}

ルーティング

routing.phpは以下のようなファイルである。繰り返しになるが、詳細はシンプルなルーティングがしたかったを読んでほしい。

$routes = [];

// トップページ "/"
$routes['index'] = ['GET', '/', function (Action $action) {
    // Chrome Loggerで確認できてべんり
    // https://craig.is/writing/chrome-logger
    chrome_log()->info('Hello, World!');
    chrome_log()->info('session', $_SESSION);

    // 200 OK で index.tpl.php を展開して返すよ、の意味
    return [200, [], view('index')];
}];

// "/acct/tadsan@pawoo.net" とか "/acct/zo@friends.nico" みたいな
$routes['acct'] = ['GET', '/acct/:acct', function (Action $action) {
    chrome_log()->info('session', $_SESSION);

    $acct_input = $action->param['acct'];

    if (!isset($_SESSION['mastodons'][$acct_input])) {
        set_flash(['error' => "Not logged in: {$acct_input}"]);
        return [302, ['Location' => '/'], null];
    };

    return [200, [], view('acct', [
        'acct' => $acct_input,
    ])];
}, ['acct' => RE_ACCT]];

// 静的ファイルを返す
$routes['license'] = ['GET', '/license', function (Action $action) {
    $path = __DIR__ . '/../../LICENSE';
    // 200 OK でプレーンテキストファイルとして
    return [200, ['Content-Type' => 'text/plain;charset=UTF-8'], file_get_contents($path)];
}];

// ...

// どれにも該当しなかったときにだけ呼ばれる特別なアクション
$routes['#404'] = function (Action $action) {
    // 404 Not Found で 404.tpl.php を展開して返すよ、の意味
    return [404, [], view('404')];
};

return $routes;

この書き方をすることで、hogehoge.phpのような格好悪いURLから卒業することができるスグレモノ。

テンプレート、再び

段取りが悪いな? さきほどは抽象的な話をしたが、こんどはもうすこし具体的に書く。

index.tpl.html
<?php
/**
 * Template for index
 *
 * @author    USAMI Kenta <tadsan@zonu.me>
 * @copyright 2017 Baguette HQ
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0
 */
/** @var $var variables */
?>
<h1>Mastodon Sample App</h1>

<ul>
    <?php foreach ($_SESSION['mastodons'] as $acct => $mastodon): ?>
        <li>
            <a href="<?= h(router()->makePath('acct', ['acct' => $acct])) ?>">
                <?= h($acct) ?>
            </a>
        </li>
    <?php endforeach; ?>
</ul>

<?php include __DIR__ . '/_login.tpl.php'; ?>

h(router()->makePath('acct', ['acct' => $acct]))これは何を意味するか。これは$acct = "tadsan@pawoo.com"であれば、/acct/tadsan@pawoo.netに展開される。これはさきほどルーティング定義した'/acct/:acct'から、逆に生成したものだ。

これはルーティング(アクセスされたURLをパラメータに分解して該当する処理を探す)の逆なので、リバースルーティング(処理の名前とパラメータをもとにアクセスさせるURLを組み立てる)とか呼ばれる。

当然だが、URLの構成情報は一度定義したのでリバースルーティングのための情報を再定義する必要はない。ルーティングの情報で十分だ。

あとがき

今回は最小のオレオレフレームワークを作るつもりで簡単なPHPアプリを構築した(完成してないけど)。コード自体は短いが、自分の書いたコードよりも運用実績のあるフレームワークの方がいいって意見があるだろう。うんうん、完全に同感。

今回の記事には、これ見よがしなつっこみどころがあるので、それが見破れない人は既存のWebフレームワークを使った方がいいです、ってことになります。Slim Frameworkとか、僕も好きだよ。

今回の記事のような内容について世の中のPHPを書く人たちがどのような感想を抱くのか気になるので、Twitterでもコメントでもどこでもいいから書いてくれたのむ。

11
14
1

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
11
14