今時php5でノーフレームワークでアップローダーを作る(1) ー ざっくり作る
まあなんかデモっぽいのが必要かなと思いましてね…
開発環境
docker-composeで作る必要がある。まずphpを処理していこう。これはまずdocker-compose.ymlというyamlファイルを作る必要がある。以下のように作ろう。
docker-compose.yml
version: '3'
services:
web:
build: ./docker/php
volumes:
- ./:/var/www/html
ports:
- 8000:80
ここではdocker/php/の下にDockerfileを置くものとする。ってわけで早速書くぞい
docker/php/Dockerfile
FROM php:5.6-apache
RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list
RUN sed -i '/stretch-updates/d' /etc/apt/sources.list
RUN sed -i '/security.debian.org/d' /etc/apt/sources.list
RUN apt update
# mysql module
#RUN docker-php-ext-install mysql # この辺は流石にmysqliに任せたい
RUN docker-php-ext-install mysqli
# set timezone
RUN echo 'date.timezone = "Asia/Tokyo"' >> /usr/local/etc/php/conf.d/docker-php-timezone.ini;
なんとなくこれが基本形のような気がする。
index.phpの作成
とりあえずdownして
% docker-compose down
動作確認用のindex.phpを作る
% echo '<?php phpinfo();' > index.php
では起動してみよう
% docker-compose up -d
とかすると普通に繋がるはずだ。
mysqlの準備
mysqlはもうawsの制約でどーしてもver8を使わざるを得なくなってるところがあるのでmysql8で用意する
version: '3'
services:
web:
build: ./docker/php
volumes:
- ./:/var/www/html
ports:
- 8000:80
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: simple_app
MYSQL_USER: user
MYSQL_PASSWORD: password
volumes:
- ./docker/mysql/data:/var/lib/mysql
とすると docker/mysql/data にmysqlのデーターが出来てくる。これは別にdocker volumeでもいいけど、まあ何となく。
composerの利用
さて、今回はPEARを使う。とはいえ、もうpearレポジトリーは息をしてないのでcomposerを使って呼びこむ必要があるが、php5はcomposer2を使えないためcomposer1を使う必要があーる。そいつの準備をしていく。
composerはzipを扱うのでunzipコマンドかphpにzip extensionを放りこむ必要があるがビルドが面倒くせえのでunzipをいれちゃおう。その他必要な設定を書いた
# composer
RUN apt install -y unzip
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin
COPY --from=composer:1 /usr/bin/composer /usr/bin/composer
ビルドをかけとく、なんなくno-cacheにした
% docker-compose build --no-cache
% docker-compose up -d
Creating network "old-style-php5-uploader_default" with the default driver
Creating old-style-php5-uploader_web_1 ... done
Creating old-style-php5-uploader_mysql_1 ... done
そうすると
% docker-compose exec web bash
root@00266484516b:/var/www/html#
でコンテナの中に入れるのでcomposerのバージョンとか確認しておこう。
PHP 5.6.40 (cli) (built: Jan 25 2019 09:50:16)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
root@00266484516b:/var/www/html# composer --version
Composer version 1.10.27 2023-09-29 10:50:23
root@00266484516b:/var/www/html# exit
exit
smartyを使ってみる
いくらoldスタイルっつってもスクリプトの中で
<?php echo "<h1>moge</h1>" ?>
みてえな奴は書きたくないってことでsmartyを使う。smartyというと旧態依然としたテンプレートエンジンの印象が拭い切れないが、実は今version5まで上がっており、php5のサポートは打ち捨てられている。php5でつかえるのはver3までであり、その最終バージョンはv3.1.48である。がまあコンテナの中で
composer require smarty/smarty
とやれば勝手に入りそうなバージョンを探してきてcomposer.jsonを作ってくれる。
root@00266484516b:/var/www/html# composer require smarty/smarty
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Using version ^3.1 for smarty/smarty
./composer.json has been created
Loading composer repositories with package information
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing smarty/smarty (v3.1.48): Downloading (100%)
Writing lock file
Generating autoload files
以下のようなファイルが出来ている
{
"require": {
"smarty/smarty": "^3.1"
}
}
ではindex.phpを更新していくぞい
<?php
require_once './vendor/autoload.php';
// Smartyオブジェクトの作成
$smarty = new Smarty();
// Smartyの各ディレクトリを設定
$smarty->template_dir = 'templates'; // テンプレートファイルのディレクトリ
$smarty->compile_dir = 'templates_c'; // コンパイル済みテンプレートのディレクトリ
// $smarty->cache_dir = 'cache'; // キャッシュファイルのディレクトリ
// $smarty->config_dir = 'configs'; // 設定ファイルのディレクトリ
// 変数を割り当てる
$smarty->assign('name', 'World');
// テンプレートを表示する
$smarty->display('index.tpl');
ここで各種ディレクトリとしてtemplatesとコンパイルディレクトリであるtemplates_c を定義した。templates_cはパーミッションの問題はややこしいのであるがとりあえずtemplates/index.tplを作る
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Smarty Example</title>
</head>
<body>
<h1>Hello, {$name}!</h1>
</body>
</html>
これでアクセスするとtemplates_cに書きこめないので、www-data権限にしたりする必要がある。まあてっとり速いのは
% sudo chown www-data templates_c -R
とかしちゃうことだろう
いよいよuploaderを作っていくぞい
index.tplでまあまあ何とかやる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>File Uploader</title>
</head>
<body>
<h1>File Uploader</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<hr>
{* アップロードされたファイルリストを表示していく *}
</body>
</html>
まあCDNとかでcssフレームワーク呼びこんでもいいっすけどね、これはあんま関係ないですし。あとでやろうかな。
var_dumper
php5で使えるdumperだとやっぱりPEARのvar_dumpってのがあったけど、これはもうcomposerから消滅しているため、別のものが必要であーる。ここではdigitalnature/php-refを使う。
% docker-compose exec web composer require digitalnature/php-ref
などとしてindex.php に
<?php
require_once './vendor/autoload.php';
// Smartyオブジェクトの作成
$smarty = new Smarty();
// Smartyの各ディレクトリを設定
$smarty->template_dir = 'templates'; // テンプレートファイルのディレクトリ
$smarty->compile_dir = 'templates_c'; // コンパイル済みテンプレートのディレクトリ
// $smarty->cache_dir = 'cache'; // キャッシュファイルのディレクトリ
// $smarty->config_dir = 'configs'; // 設定ファイルのディレクトリ
// 変数を割り当てる
$smarty->assign('name', 'World');
// テンプレートを表示する
$smarty->display('index.tpl');
r($_FILES);
とでも書いとてみよう
そしたら
こんなのが出てくるし、さらにファイルをアップすると
こんな具合になるわけだ
$_FILESがあれば保存する
、の前に
保存ディレクトリをconfigに持たせてあげる。ここではPEARが使えるものは(っていうかcomposerに残ってるものは)徹底的に使っていく指針ということでConfig_Liteが残っているのでそれを使うことにする。
% docker-compose exec web composer require pear/config_lite
地味にdosのini形式を読んだりするので、それを作る
config.ini
upload_dir = 'stored/'
とか書いといて
<?php
require_once './vendor/autoload.php';
$config = new Config_Lite('config.ini');
とすればupload_dirがセットされるからアップロードがあった場合の処理を書くと
<?php
require_once './vendor/autoload.php';
$config = new Config_Lite('config.ini');
// Smartyオブジェクトの作成
$smarty = new Smarty();
// Smartyの各ディレクトリを設定
$smarty->template_dir = 'templates'; // テンプレートファイルのディレクトリ
$smarty->compile_dir = 'templates_c'; // コンパイル済みテンプレートのディレクトリ
// $smarty->cache_dir = 'cache'; // キャッシュファイルのディレクトリ
// $smarty->config_dir = 'configs'; // 設定ファイルのディレクトリ
// 変数を割り当てる
$smarty->assign('name', 'World');
// テンプレートを表示する
$smarty->display('index.tpl');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
このようにgetできるわけだ、きちーなーw
ちなみに、この設定ファイルをindex.phpと同列に置いてるとアクセスされる可能性が高いんでそういうのも考えないといけないからね。まあ今はいいけど。
ファイル保存テーブル
いよいよDBに書いていくことになるが、それにあたってはスキーマの定義が必要である。今回はこのようにメタデーターを書くテーブルを用意した
CREATE TABLE uploaded_files (
id INT AUTO_INCREMENT PRIMARY KEY,
original_name VARCHAR(255) NOT NULL,
saved_name VARCHAR(255) NOT NULL,
mime_type VARCHAR(50),
size INT NOT NULL,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
これをinit/schema.sql としている。
そしたらmysqlコマンドでこれを流しこむ
% docker-compose exec -T mysql mysql -uuser -ppassword simple_app < init/schema.sql
ひとまず準備ができたのでDBに接続していく
PEAR MDB2
これはまだギリギリcomposerにあるので、とんどん投入していくぞい。
mdb2の供給者も複数あったりして流石クソみたいな野良パッケージやなあと思うが、まあ仕方ない。DLの多いのを使う
% docker-compose exec web composer require nanasess/mdb2
てか、何でだったかな、dev-masterに後から切り替えた気がします。最後にjson書いときますわ
まあどう見てもヤバい状況なんだけどやっぱnanasessって人のが楽だった
% docker-compose exec web composer require nanasess/mdb2_driver_mysqli
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Using version ^1.5 for nanasess/mdb2_driver_mysqli
./composer.json has been updated
Loading composer repositories with package information
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing nanasess/mdb2_driver_mysqli (v1.5.4): Downloading (100%)
Package nanasess/mdb2_driver_mysqli is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating autoload files
まあどっちにせよこんなのはどれを取ってもabandonなので 気にする必要はない。これも確かdev-masterに切り替えたと思う
{
"require": {
"smarty/smarty": "^3.1",
"digitalnature/php-ref": "^1.3",
"pear/config_lite": "^0.2.6",
"nanasess/mdb2": "dev-master",
"nanasess/mdb2_driver_mysqli": "^1.5"
}
}
繋いでみる
では繋いでみよう。の前に設定をconfig.iniに仕込む
upload_dir = 'stored/'
db_dsn = 'mysqli://user:password@mysql/simple_app?charset=utf8'
db_dsnを取り出して繋いでみると…
<?php
require_once './vendor/autoload.php';
$config = new Config_Lite('config.ini');
// Connect to Database
$dsn = $config->get(null, 'db_dsn');
$mdb2 = MDB2::connect($dsn);
if (PEAR::isError($mdb2)) {
// rtはtext形式で出力
rt($mdb2->getDebugInfo());
exit;
}
いつもの
> $mdb2->getDebugInfo() - /var/www/html/index.php:12
======================================================
string(265) "_doConnect: [Error message: Server sent charset unknown to the client. Please, report to the developers]
[Native code: 2054]
[Native message: Server sent charset unknown to the client. Please, report to the developers]
** mysqli(mysqli)://user:xxx@mysql/simple_app"
のように綺麗に失敗するのでdocker-compose.ymlを改善する
docker-compose.ymlの改善
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: simple_app
MYSQL_USER: user
MYSQL_PASSWORD: password
volumes:
- ./docker/mysql/data:/var/lib/mysql
- ./docker/mysql/local.cnf:/etc/mysql/conf.d/local.cnf
このようにlocal.cnfを割当ててしまう。というわけでdocker/mysql/local.cnf
[mysqld]
default-time-zone = 'Asia/Tokyo'
character-set-server=utf8
collation-server=utf8_unicode_ci
default-authentication-plugin = mysql_native_password
sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
のようにしたら
% docker-compose restart
Restarting old-style-php5-uploader_web_1 ... done
Restarting old-style-php5-uploader_mysql_1 ... done
これでbuildして再起動すると…
> $mdb2->getDebugInfo() - /var/www/html/index.php:13
======================================================
string(243) "_doConnect: [Error message: The server requested authentication method unknown to the client]
[Native code: 2054]
[Native message: The server requested authentication method unknown to the client]
** mysqli(mysqli)://user:xxx@mysql/simple_app"
などと言われる。まあこれは認証の問題であーる。
% sudo rm -rf docker/mysql/data
% docker-compose up -d
Creating network "old-style-php5-uploader_default" with the default driver
Creating old-style-php5-uploader_mysql_1 ... done
Creating old-style-php5-uploader_web_1 ... done
みたいにdocker/mysql/data を全部消して作り直した方が早いかもわからんが、これをやったら
% docker-compose exec -T mysql mysql -uuser -ppassword simple_app < init/schema.sql
これも忘れぬように
いよいよinsertする
PEAR MDB2を使いこなしてる者(そんな奴おったんか?)ならprepareとかしないで、extendedモジュールをloadしてautoexecuteしたくなるだろう。
さらに別に1つのテーブルだからtransactionかけなくてもいいけどテスト的にtransactionをかけつつ保存してみて、とりだしてみるみたいなこともしてみる。
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
// Insert meta data here
$mdb2->loadModule('Extended');
$mdb2->beginTransaction();
$fileData = array(
'original_name' => $_FILES['file']['name'],
'saved_name' => basename($_FILES['file']['name']),
'mime_type' => $_FILES['file']['type'],
'size' => $_FILES['file']['size']
);
// autoExecute を使用してデータベースに挿入
$result = $mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
$sql = "SELECT * FROM uploaded_files";
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
r($mdb2->queryAll($sql));
if (PEAR::isError($result)) {
die($result->getMessage());
}
exit;
この時点でいろいろ問題あり杉内なんじゃないかとも思えるが、まあ原始的なやり方だとこんな感じだろう
ここでoriginal_nameは保存するんだけども保存名はたとえばID5桁+拡張子としたいとかいう場合を考えてみると
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
// データベースのExtendedモジュールをロード
$mdb2->loadModule('Extended');
// トランザクション開始
$mdb2->beginTransaction();
$fileData = array(
'original_name' => $_FILES['file']['name'],
'saved_name' => basename($_FILES['file']['name']), // 一時的な名前
'mime_type' => $_FILES['file']['type'],
'size' => $_FILES['file']['size']
);
// ファイルメタデータをデータベースに挿入
$result = $mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
if (PEAR::isError($result)) {
$mdb2->rollback(); // エラーがあればロールバック
die($result->getMessage());
}
// 挿入されたレコードのIDを取得
$id = $mdb2->lastInsertId('uploaded_files', 'id');
// ファイル拡張子を取得
$fileExt = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
// 保存するファイル名をフォーマット
$savedName = sprintf('%05d.%s', $id, $fileExt);
// saved_nameを更新
$updateResult = $mdb2->query("UPDATE uploaded_files SET saved_name = '$savedName' WHERE id = $id");
if (PEAR::isError($updateResult)) {
$mdb2->rollback(); // エラーがあればロールバック
die($updateResult->getMessage());
}
// トランザクションコミット
// $mdb2->commit();
// ファイルを指定されたディレクトリに保存
if (!move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $savedName)) {
die('Failed to move uploaded file.');
}
header('Location: index.php');
exit;
}
まあこんな感じのコードになるだろう。
session flash メッセージ
session_start();
とかして
// ファイルを指定されたディレクトリに保存
if (!move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $savedName)) {
die('Failed to move uploaded file.');
}
$_SESSION['flash_message'] = 'ファイルが正常にアップロードされました。';
header('Location: index.php');
exit;
からの
$flashMessage = null;
if (isset($_SESSION['flash_message'])) {
$flashMessage = $_SESSION['flash_message']; // メッセージを取得
unset($_SESSION['flash_message']);
}
// テンプレートを表示する
$smarty->assign('flashMessage', $flashMessage);
$smarty->display('index.tpl');
ってなもんだ
アップロードされたファイル一覧の取り出し
ラストもう一歩
// データベースからアップロードされたファイルのメタデータを取得
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
$files = $mdb2->queryAll("SELECT * FROM uploaded_files ORDER BY uploaded_at DESC");
// テンプレートを表示する
$smarty->assign('flashMessage', $flashMessage);
$smarty->assign('files', $files);
$smarty->display('index.tpl');
テンプレート index.tpl
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>File Uploader</title>
</head>
<body>
<p>{$flashMessage}</p>
<h1>File Uploader</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<hr>
{* アップロードされたファイルリストを表示していく *}
{if $files|@count > 0}
<table border="1">
<thead>
<tr>
<th>ファイル</th>
<th>サイズ</th>
<th>アップロード日時</th>
</tr>
</thead>
<tbody>
{foreach from=$files item=file}
<tr>
<td>
<a href="#">{$file.saved_name}</a>
</td>
<td>{$file.size} bytes</td>
<td>{$file.uploaded_at}</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<p>アップロードされたファイルはありません。</p>
{/if}
</body>
</html>
結果
最終ソース
index.php
<?php
require_once './vendor/autoload.php';
$config = new Config_Lite('config.ini');
// Connect to Database
$dsn = $config->get(null, 'db_dsn');
$mdb2 = MDB2::connect($dsn);
if (PEAR::isError($mdb2)) {
// rtはtext形式で出力
rt($mdb2->getDebugInfo());
exit;
}
// Smartyオブジェクトの作成
$smarty = new Smarty();
// Smartyの各ディレクトリを設定
$smarty->template_dir = 'templates'; // テンプレートファイルのディレクトリ
$smarty->compile_dir = 'templates_c'; // コンパイル済みテンプレートのディレクトリ
$smarty->cache_dir = 'cache'; // キャッシュファイルのディレクトリ
$smarty->config_dir = 'configs'; // 設定ファイルのディレクトリ
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
// データベースのExtendedモジュールをロード
$mdb2->loadModule('Extended');
// トランザクション開始
$mdb2->beginTransaction();
$fileData = array(
'original_name' => $_FILES['file']['name'],
'saved_name' => basename($_FILES['file']['name']), // 一時的な名前
'mime_type' => $_FILES['file']['type'],
'size' => $_FILES['file']['size']
);
// ファイルメタデータをデータベースに挿入
$result = $mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
if (PEAR::isError($result)) {
$mdb2->rollback(); // エラーがあればロールバック
die($result->getMessage());
}
// 挿入されたレコードのIDを取得
$id = $mdb2->lastInsertId('uploaded_files', 'id');
// ファイル拡張子を取得
$fileExt = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
// 保存するファイル名をフォーマット
$savedName = sprintf('%05d.%s', $id, $fileExt);
// saved_nameを更新
$updateResult = $mdb2->query("UPDATE uploaded_files SET saved_name = '$savedName' WHERE id = $id");
if (PEAR::isError($updateResult)) {
$mdb2->rollback(); // エラーがあればロールバック
die($updateResult->getMessage());
}
// トランザクションコミット
$mdb2->commit();
// ファイルを指定されたディレクトリに保存
if (!move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $savedName)) {
die('Failed to move uploaded file.');
}
$_SESSION['flash_message'] = 'ファイルが正常にアップロードされました。';
header('Location: index.php');
exit;
}
$flashMessage = null;
if (isset($_SESSION['flash_message'])) {
$flashMessage = $_SESSION['flash_message']; // メッセージを取得
unset($_SESSION['flash_message']);
}
// データベースからアップロードされたファイルのメタデータを取得
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
$files = $mdb2->queryAll("SELECT * FROM uploaded_files ORDER BY uploaded_at DESC");
// テンプレートを表示する
$smarty->assign('flashMessage', $flashMessage);
$smarty->assign('files', $files);
$smarty->display('index.tpl');
templates/index.tpl
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>File Uploader</title>
</head>
<body>
<p>{$flashMessage}</p>
<h1>File Uploader</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<hr>
{* アップロードされたファイルリストを表示していく *}
{if $files|@count > 0}
<table border="1">
<thead>
<tr>
<th>ファイル</th>
<th>サイズ</th>
<th>アップロード日時</th>
</tr>
</thead>
<tbody>
{foreach from=$files item=file}
<tr>
<td>
<a href="#">{$file.saved_name}</a>
</td>
<td>{$file.size} bytes</td>
<td>{$file.uploaded_at}</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<p>アップロードされたファイルはありません。</p>
{/if}
</body>
</html>
非常〜につっこみ所しかないクラシカルなソースコードであるが、15年くらい前はこのようなソースコードが普通に見られたし、そもそもsmartyすら使われていなかったかもしれない。
これを教材にして次回はもう少し改善させてみよう。そして、最終的にはこれをAmazon ECSで動かすってところまでやるからね。
この記事が気に入ったらサポートをしてみませんか?