PHP构建笔记:用于演示库的PHP应用
由于最近越来越不理解PHP的写作方式,我决定开始记录自己设计的代码。
使用框架构建PHP应用程序的构建技术相对较容易被共享的知识,但是对于不依赖框架的PHP构建而言,似乎缺乏集中的资料,因此需要共享使用案例。
我之前写的文章
-
- 「インスパイヤされて掲示板を作りたくなった(1)」 シリーズ
- 「レガシーなプロダクトの改善 フレームワークを利用できない環境でのライブラリ活用」 (WEB+DB PRESS Vol.96所収)
简言之
これは何?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
内部/
这里有五个文件。
functions.php
Webサービスとしての汎用的なユーティリティ函数を定義apps.php
アプリケーションのための関数routes.php
ルーティングごとの処理をクロージャで定義variables.php
データをつっこむ謎コンテナ(謎)bootstrap.php
上記のファイルとかComposerのオートローダーをまとめて読み込んで、実行の前処理をするfunctions.php和apps.php之间的区别可能有些模糊,但涉及应用程序领域(在这里是为了Mastodon的处理)的内容被归类到app.php中,而涉及其他Web应用程序的通用处理(可以移植到不同目的的应用程序并使用)的内容则被归类到functions.php中。
routes.php用于定义路由。详细信息请阅读“希望实现简单路由”的相关内容。将routes.php进行分割的想法是从Slim框架借鉴而来的。
variables.php不是用来定义全局变量等等,而是定义一个用于存储变量的variable类。如果遵循PSR-1基本编码规范(日语),类名应该使用驼峰命名法,但在这里完全忽略了这一规范,因为这是我的世界。
既然提到了命名空间,那么我顺便在这里提一下,app.php中定义的函数属于app命名空间,而其他的基本上都是使用顶层命名空间。
观看
在HTML模板中直接使用了PHP。为了区分脚本处理的PHP文件(不包括HTML),将其扩展名设为.tpl.php。
在不具备类似PHP的继承概念的模板引擎中,通常有以下几种模式来实现页面模板的共享化。
A. 全ページのテンプレートからヘッダとフッタをincludeする
B. 共通ページテンプレートから固有のテンプレートをincludeする
把这个转化成代码后,会变成如下所示。
<?php include isset($header_tpl) ? $header_tpl : __DIR__ . '/header.php' ?>
<main>
<!-- それぞれのページのメインコンテンツ -->
</main>
<?php include isset($header_tpl) ? $header_tpl : __DIR__ . '/header.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>
你能理解这种A和B的模式是分别从各自的模板中读取公共部分,并从公共模板中读取特定部分的方向是相反的吗?
特别大的原因虽然没有,但这次决定采用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中。
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();
});
只是启用了flipping/whoops而已,并没有做什么特别的事情。这次决定只在开发环境中启用,虽然在生产环境中也有使用的方法,但在此不详述。关于这个内容,已经在《WEB+DB PRESS Vol.96》中写过了。
入口
唐诺顿-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的绝佳方法。
模板,再次出现。
“安排不太好了吧?刚才谈得有点抽象,这次我会更具体地写一些。”
<?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的构成信息已经定义过了,所以不需要重新定义用于反向路由的信息。路由信息就足够了。
后记
这次我打算构建一个简单的PHP应用程序,目的是创建一个最小的自制框架(虽然还没有完成)。虽然代码本身很短,但是有些人可能认为使用有实际运营经验的框架比使用自己编写的代码更好。嗯嗯,我完全同意。
这篇文章中有明显的批评点,如果不能识破的人最好使用现有的Web框架,比如Slim Framework,我也很喜欢它。
我很想知道在像这篇文章一样的内容上,世界上写PHP的人对此有什么感想,无论是在Twitter上还是在任何地方,请写下你的评论。