今時php5でノーフレームワークでアップローダーを作る(3) ー オブジェクト思考-class化
phpに限らずオブジェクト思考でアプリ組めますか?設計できますか?使えますか?ってのをイチからやってきます。初学者の人はよくみといてください。わかってりゃここはあんま見る必要ない。
現状の把握
まず、今のindex.phpを全文貼り付ける
<?php
require_once './vendor/autoload.php';
$config = new Config_Lite(__DIR__. '/config/config.ini');
// ロガーの設定
$logDir = $config->get(null, 'log_dir');
$logger = Log::singleton('file', $logDir. '/app.log', 'ident', array('mode' => 0600, 'timeFormat' => '%X %x'));
// 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();
use ByteUnits\Metric;
function formatFileSize($size) {
return Metric::bytes($size)->format();
}
// Smartyにプラグインとして関数を登録
$smarty->registerPlugin('modifier', 'formatFileSize', 'formatFileSize');
// 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'])) {
$logger->info('ファイルアップロード処理が開始されました。');
$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.');
}
$logger->info('ファイルアップロード処理が正常に終了しました。');
$_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");
// アップロードされたファイルへの直接リンクを作成
foreach ($files as &$file) {
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
$file['url'] = $uploadDir . $file['saved_name']; // 'uploads/'ディレクトリを想定
}
if (isset($_GET['action']) && $_GET['action'] == 'delete') {
if (!isset($_GET['id']) || empty($_GET['id'])) {
die("ID not found");
}
// IDを安全に取得する
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
// ファイルをファイルシステムから削除
$filePath = $config->get(null, 'upload_dir') . $file['saved_name'];
if (file_exists($filePath)) {
unlink($filePath);
}
// データベースからファイル情報を削除
$sql = "DELETE FROM uploaded_files WHERE id = ".$mdb2->quote($id, 'integer');
$result = $mdb2->query($sql);
if (PEAR::isError($result)) {
die($result->getMessage());
}
$logger->info('ID: '.$id.' ファイル削除処理が正常に終了しました。');
$_SESSION['flash_message'] = 'ファイルを正常に削除しました。';
header('Location: index.php');
exit;
}
// テンプレートを表示する
$smarty->assign('flashMessage', $flashMessage);
$smarty->assign('files', $files);
$smarty->display('index.tpl');
これは典型的な手続き型プログラミングである。同じような処理が重複しているし、actionに応じての処理がなんかdeleteだけ走ったりしているし、とにかくいろいろよろしいとは思えない。この状態で改良していってもいいんだけど、せっかくだしまず基本的にこういう場合はclassを使った設計に持ち込むべきである。
こういう場合とりあえず、まず全部コメントアウトする。
<?php
require_once './vendor/autoload.php';
/* autoloadの読み込みを残して全部なかったことにする。
classを作る
classを1から作って再設計するというミッションはまあなかなか携わる事もないだろうし、ここではしっかりやってみよう。
ここではSimpleUploader という名前のclassにしてみる。実際にはindex.phpとは分離した方がいいかもだが、ここではindex.phpにまとめて書く事とする
<?php
require_once './vendor/autoload.php';
class SimpleUploader
{
public function __construct()
{
}
}
$uploader = new SimpleUploader();
でまあ冒頭伸べた通り、これは初心者向けのドキュントを想定しているのでちゃんと書くけど、これは現在何もないclassを定義している。ここで
$uploader = new SimpleUploader();
newとすることでこのSimpleUploaderから操作できるヤツを作っている。この際にphp5からは__construct() という名前の関数(ただし、class内関数のことをphpでは「メソッド」と読んでいる)が自動的にcallされる。
これのコンストラクターの概念はオブジェクト指向プログラミング(oopとも言う)の場合大抵有益であり、ここに初期化処理を書いていく事になるわけだ。前のコードだと初期化という概念がなかったでしょ。まあ強いてやるならfunction init() {}とかそういう関数を作ってもいいんだけどもっていう..
ちなみにデストラクターってのもあるけどとりあえずよしとする。
コンストラクターに移動するべき初期化処理
たとえば
$config = new Config_Lite(__DIR__. '/config/config.ini');
// ロガーの設定
$logDir = $config->get(null, 'log_dir');
$logger = Log::singleton('file', $logDir. '/app.log', 'ident', array('mode' => 0600, 'timeFormat' => '%X %x'));
このようにアプリケーションの起動で大体使う処理はコンストラクターに移動する。この場合、$loggerなどは自分自身のobjectにつつみこんでしまう。これをカプセル化(Encapsulation)という。これを実現したコードを以下に示す
require_once './vendor/autoload.php';
class SimpleUploader
{
private $logger;
private $config;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
}
}
$uploader = new SimpleUploader();
ここではprivate というキーワードを用いてこのclass内からのアクセスに限定している。まあ初学者の場合はとりあえずこういうもんだと思っておけばokである。ちなみに、class内でnewすると面倒くさいことになる事もあるが、とりあえずは良しとしてしまおう。前の手続き型のベタっとした感じをとにかくclassに移動していく。まだこの時点で概念が付かめてなくてokである(そもそも実行すらしていない)
executeメソッドを作る
今実際に
$uploader = new SimpleUploader();
これを行って初期化しているだけなので何もしない。ここではexecute() というメソッドを作って処理を行ってみよう。
class SimpleUploader
{
private $logger;
private $config;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
}
public function execute()
{
}
}
$uploader = new SimpleUploader();
$uploader->execute();
これを行う事で処理はexecute() に移動する。つまり
$uploader = new SimpleUploader();
これでcontructorがコールされ
$uploader->execute();
これでexecute()がコールされる。
ところで
$uploader = new SimpleUploader();
$uploader->execute();
$uploader2 = new SimpleUploader();
$uploader2->execute();
とした場合はどうだろう。実はこれはアプリが2つ起動する、こういうケースはwebアプリではまず見掛けないが、このようなこともできると考えといてよい
smartyテンプレートエンジンの移動
ここで、まず
// テンプレートを表示する
$smarty->assign('flashMessage', $flashMessage);
$smarty->assign('files', $files);
$smarty->display('index.tpl');
この辺のことを行ってみよう。この処理もベタっと書くとアレなので
class SimpleUploader
{
private $logger;
private $config;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
}
// ここでuploadフォームとファイルリストを表示する
private function display()
{
}
public function execute()
{
$this->display();
}
}
このdisplay()メソッド(関数)に移動していく
class SimpleUploader
{
private $logger;
private $config;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
}
// ここでuploadフォームとファイルリストを表示する
private function display()
{
$smarty->assign('flashMessage', $flashMessage);
$smarty->assign('files', $files);
$smarty->display('index.tpl');
}
public function execute()
{
$this->display();
}
}
$uploader = new SimpleUploader();
$uploader->execute();
これは見ての通り $smarty が未定義である。つまりこれも何とかしておかないといけない。これも通常はコンストラクタでカプセル化する
<?php
require_once './vendor/autoload.php';
class SimpleUploader
{
private $logger;
private $config;
private $smarty;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
// Smartyインスタンスの初期化
$this->smarty = new Smarty();
$this->smarty->template_dir = 'templates';
$this->smarty->compile_dir = 'templates_c';
}
private function display()
{
$flashMessage = $files = null;
$this->smarty->assign('flashMessage', $flashMessage);
$this->smarty->assign('files', $files);
$this->smarty->display('index.tpl');
}
public function execute()
{
$this->display();
}
}
$uploader = new SimpleUploader();
$uploader->execute();
このようにprivateのsmartyにsmartyをカプセルする。displayでは$this-> を通じてcallする。なお、flassMessageとfilesは今何もないのでとりあえずnullをつっこんでいる。
また
<td>{$file.size} bytes</td>
{* <td>{$file.size|formatFileSize}</td> *}
このformatFileSize はまだつっこんでないのでエラーとなるので元に戻しておく
そうすれば
この画面は出てくるはずだ。
ファイルの保存をちょっと書き直す
ここでsmartyのformを見てみよう
templates/index.tpl
<form action="index.php" method="post" enctype="multipart/form-data">
これを
<form action="index.php?action=save" method="post" enctype="multipart/form-data">
このように書き換えてpostしてみようするとURLが
http://server/index.php?action=save
このようなURLに変化するはずだ。このactionの中身を取ってきて処理を分岐していくように変更する。
executeメソッドの修正
ここではswitch caseを使っている。if文でもまあいいっちゃいい
public function execute()
{
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
case 'save':
// ここで保存
break;
default:
$this->display();
}
}
display()メソッドはcontrollerのactionに近いようや処理になるがそもそもController単位でclassを切ってないし、あんまフレームワーク的な話はここではいい。とりあえずここでsaveを行うメソッドを書いてみる
private function save()
{
}
public function execute()
{
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
case 'save':
$this->save();
break;
default:
$this->display();
}
}
saveメソッドで実際にDBに保存する
まあこれも機能を移動するだけではあるが
private function save()
{
$logger->info('ファイルアップロード処理が開始されました。');
$uploadDir = $config->get(null, 'upload_dir'); // -> stored
// データベースのExtendedモジュールをロード
$mdb2->loadModule('Extended');
// ...ガバっとコピー
このようにコピーしてくる。しかしこのままでは当然動作しない
まず$loggerは$this->loggerに切り替える必要があるし、$configも$this->configに切り替えるべきだろう
private function save()
{
$this->logger->info('ファイルアップロード処理が開始されました。');
$uploadDir = $this->config->get(null, 'upload_dir');
// データベースのExtendedモジュールをロード
$mdb2->loadModule('Extended');
でまあ問題は$mdb2でこれもコンストラクターで処理しないといけないよな。
mdb2の処理
つわけでこれも同様にcontructorで処理する
class SimpleUploader
{
private $logger;
private $config;
private $smarty;
private $mdb2;
としておいて
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
// Smartyインスタンスの初期化
$this->smarty = new Smarty();
$this->smarty->template_dir = 'templates';
$this->smarty->compile_dir = 'templates_c';
// データベース接続の初期化
$dsn = $this->config->get(null, 'db_dsn');
$this->mdb2 = MDB2::connect($dsn);
if (PEAR::isError($this->mdb2)) {
$this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
throw new Exception("データベース接続に失敗しました");
}
}
などとする。ここでthrowってのが出てきたね〜これはまあとりあえず何もしなければ単純にエラーで止まると思っとけばいい。ブラウザーには以下のように出てくるはずだ
Fatal error: Uncaught exception 'Exception' with message 'データベース接続に失敗しました' in /var/www/html/index.php:29 Stack trace: #0 /var/www/html/index.php(109): SimpleUploader->__construct() #1 {main} thrown in /var/www/html/index.php on line 29
でもこれは開発中ならいいけど本番でこれが出るのはマズいなーってこともあるでしょうからね。まあその場合の話は次回考えていこう、
でまあガンガン書き換えていく
private function save()
{
$this->logger->info('ファイルアップロード処理が開始されました。');
$uploadDir = $this->config->get(null, 'upload_dir');
// データベースのExtendedモジュールをロード
$this->mdb2->loadModule('Extended');
// トランザクション開始
$this->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 = $this->mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
die($result->getMessage());
}
問題はこのisErrorで、これもclassにしたならdie()とかじゃなくてthrowすべきだろう。
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("更新処理に失敗しました: " . $result->getDebugInfo());
throw new Exception($result->getDebugInfo());
}
敢えて失敗してみせると
Fatal error: Uncaught exception 'Exception' with message 'prepare: [Error message: Unable to create prepared statement handle] [Native code: 1146] [Native message: Table 'simple_app.uploaded_file' doesn't exist] ' in /var/www/html/index.php:63 Stack trace: #0 /var/www/html/index.php(100): SimpleUploader->save() #1 /var/www/html/index.php(109): SimpleUploader->execute() #2 {main} thrown in /var/www/html/index.php on line 63
などとなる。
まあ長々と書いても仕方ないのでsave() メソッドを全部書いとく
private function save()
{
$this->logger->info('ファイルアップロード処理が開始されました。');
$uploadDir = $this->config->get(null, 'upload_dir');
// データベースのExtendedモジュールをロード
$this->mdb2->loadModule('Extended');
// トランザクション開始
$this->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 = $this->mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("更新処理に失敗しました: " . $result->getDebugInfo());
throw new Exception($result->getDebugInfo());
}
// 挿入されたレコードのIDを取得
$id = $this->mdb2->lastInsertId('uploaded_files', 'id');
// ファイル拡張子を取得
$fileExt = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
// 保存するファイル名をフォーマット
$savedName = sprintf('%05d.%s', $id, $fileExt);
// saved_nameを更新
$updateResult = $this->mdb2->query("UPDATE uploaded_files SET saved_name = '$savedName' WHERE id = $id");
if (PEAR::isError($updateResult)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("保存ファイル名の処理に失敗しました: " . $updateResult->getDebugInfo());
throw new Exception($updateResult->getDebugInfo());
}
// ファイルを指定されたディレクトリに保存
if (!move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $savedName)) {
throw new Exception('Failed to move uploaded file.');
}
// トランザクションコミット
$this->mdb2->commit();
$this->logger->info('ファイルアップロード処理が正常に終了しました。');
$_SESSION['flash_message'] = 'ファイルが正常にアップロードされました。';
header('Location: index.php');
exit;
}
displayメソッドを更新してファイルリストとかをちゃんと取る
というかまずsessionをstartしないといけないね。これは先頭で行っている
<?php
session_start();
require_once './vendor/autoload.php';
class SimpleUploader
{
// 略
そしたらlistメソッドでわけわからんくなってるところ
private function display()
{
$flashMessage = $files = null; // これ
$this->smarty->assign('flashMessage', $flashMessage);
$this->smarty->assign('files', $files);
$this->smarty->display('index.tpl');
}
をちゃんとする。
private function display()
{
$flashMessage = null;
if (isset($_SESSION['flash_message'])) {
$flashMessage = $_SESSION['flash_message']; // メッセージを取得
unset($_SESSION['flash_message']);
}
// データベースからアップロードされたファイルのメタデータを取得
$this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
$files = $this->mdb2->queryAll("SELECT * FROM uploaded_files ORDER BY uploaded_at DESC");
// アップロードされたファイルへの直接リンクを作成
foreach ($files as &$file) {
$uploadDir = $this->config->get(null, 'upload_dir'); // -> stored
$file['url'] = $uploadDir . $file['saved_name']; // 'uploads/'ディレクトリを想定
}
$this->smarty->assign('flashMessage', $flashMessage);
$this->smarty->assign('files', $files);
$this->smarty->display('index.tpl');
}
まあ基本的にはコードを持ってきただけともいえるが。
削除処理
そしたら削除だが今これはaction=deleteがつくようになっているので
private function delete()
{
}
public function execute()
{
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
case 'save':
$this->save();
break;
case 'delete': // これ
$this->delete();
break;
default:
$this->display();
}
}
これを生やす
private function delete()
{
if (!isset($_GET['id']) || empty($_GET['id'])) {
die("ID not found");
}
// IDを安全に取得する
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
$this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
$file = $this->mdb2->queryRow("SELECT * FROM uploaded_files WHERE id = ". $this->mdb2->quote($id, 'integer'));
if (!$file) {
$_SESSION['flash_message'] = '指定されたファイルが見付かりませんでした';
header('Location: index.php');
}
// ファイルをファイルシステムから削除
$filePath = $this->config->get(null, 'upload_dir') . $file['saved_name'];
if (file_exists($filePath)) {
unlink($filePath);
}
// データベースからファイル情報を削除
$sql = "DELETE FROM uploaded_files WHERE id = ". $this->mdb2->quote($id, 'integer');
$result = $this->mdb2->query($sql);
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("ファイル削除に失敗しました: " . $result->getDebugInfo());
throw new Exception($result->getDebugInfo());
}
$this->logger->info('ID: '.$id.' ファイル削除処理が正常に終了しました。');
$_SESSION['flash_message'] = 'ファイルを正常に削除しました。';
header('Location: index.php');
exit;
}
setFetchModeをイチイチ書くのがダルい
という場合はもうコンストラクターでセットしちゃっていいと思うよ〜
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
// Smartyインスタンスの初期化
$this->smarty = new Smarty();
$this->smarty->template_dir = 'templates';
$this->smarty->compile_dir = 'templates_c';
// データベース接続の初期化
$dsn = $this->config->get(null, 'db_dsn');
$this->mdb2 = MDB2::connect($dsn);
if (PEAR::isError($this->mdb2)) {
$this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
throw new Exception("データベース接続に失敗しました");
}
$this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
}
最後にsmartyのプラグインを定義する
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
// Smartyインスタンスの初期化
$this->smarty = new Smarty();
$this->smarty->template_dir = 'templates';
$this->smarty->compile_dir = 'templates_c';
$this->smarty->registerPlugin('modifier', 'formatFileSize', [$this, 'formatFileSize']);
// データベース接続の初期化
$dsn = $this->config->get(null, 'db_dsn');
$this->mdb2 = MDB2::connect($dsn);
$this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
if (PEAR::isError($this->mdb2)) {
$this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
throw new Exception("データベース接続に失敗しました");
}
}
public function formatFileSize($size)
{
return Metric::bytes($size)->format();
}
自分の中の関数に入れる場合はarrayで$this を指定するなお[]記法は5.4から利用可能である。
いや、最後にもうちょっと改善
formでファイルが入ってないのにsubmitが押さとき
private function save()
{
if (empty($_FILES['file']['name'])) {
$_SESSION['flash_message'] = 'ファイルが選択されていません。';
header('Location: index.php');
exit;
}
ちなみにhtml側では既にrequiredになっていたw
最終ソースコード
<?php
session_start();
require_once './vendor/autoload.php';
use ByteUnits\Metric;
class SimpleUploader
{
private $logger;
private $config;
private $smarty;
private $mdb2;
public function __construct()
{
$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
$logDir = $this->config->get(null, 'log_dir');
$this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
'mode' => 0600,
'timeFormat' => '%X %x'
]);
// Smartyインスタンスの初期化
$this->smarty = new Smarty();
$this->smarty->template_dir = 'templates';
$this->smarty->compile_dir = 'templates_c';
$this->smarty->registerPlugin('modifier', 'formatFileSize', [$this, 'formatFileSize']);
// データベース接続の初期化
$dsn = $this->config->get(null, 'db_dsn');
$this->mdb2 = MDB2::connect($dsn);
if (PEAR::isError($this->mdb2)) {
$this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
throw new Exception("データベース接続に失敗しました");
}
$this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
}
public function formatFileSize($size)
{
return Metric::bytes($size)->format();
}
private function display()
{
$flashMessage = null;
if (isset($_SESSION['flash_message'])) {
$flashMessage = $_SESSION['flash_message']; // メッセージを取得
unset($_SESSION['flash_message']);
}
// データベースからアップロードされたファイルのメタデータを取得
$files = $this->mdb2->queryAll("SELECT * FROM uploaded_files ORDER BY uploaded_at DESC");
// アップロードされたファイルへの直接リンクを作成
foreach ($files as &$file) {
$uploadDir = $this->config->get(null, 'upload_dir'); // -> stored
$file['url'] = $uploadDir . $file['saved_name']; // 'uploads/'ディレクトリを想定
}
$this->smarty->assign('flashMessage', $flashMessage);
$this->smarty->assign('files', $files);
$this->smarty->display('index.tpl');
}
private function save()
{
if (empty($_FILES['file']['name'])) {
$_SESSION['flash_message'] = 'ファイルが選択されていません。';
header('Location: index.php');
exit;
}
$this->logger->info('ファイルアップロード処理が開始されました。');
$uploadDir = $this->config->get(null, 'upload_dir');
// データベースのExtendedモジュールをロード
$this->mdb2->loadModule('Extended');
// トランザクション開始
$this->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 = $this->mdb2->extended->autoExecute('uploaded_files', $fileData, MDB2_AUTOQUERY_INSERT);
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("更新処理に失敗しました: " . $result->getDebugInfo());
throw new Exception($result->getDebugInfo());
}
// 挿入されたレコードのIDを取得
$id = $this->mdb2->lastInsertId('uploaded_files', 'id');
// ファイル拡張子を取得
$fileExt = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
// 保存するファイル名をフォーマット
$savedName = sprintf('%05d.%s', $id, $fileExt);
// saved_nameを更新
$updateResult = $this->mdb2->query("UPDATE uploaded_files SET saved_name = '$savedName' WHERE id = $id");
if (PEAR::isError($updateResult)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("保存ファイル名の処理に失敗しました: " . $updateResult->getDebugInfo());
throw new Exception($updateResult->getDebugInfo());
}
// ファイルを指定されたディレクトリに保存
if (!move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $savedName)) {
throw new Exception('Failed to move uploaded file.');
}
// トランザクションコミット
$this->mdb2->commit();
$this->logger->info('ファイルアップロード処理が正常に終了しました。');
$_SESSION['flash_message'] = 'ファイルが正常にアップロードされました。';
header('Location: index.php');
exit;
}
private function delete()
{
if (!isset($_GET['id']) || empty($_GET['id'])) {
die("ID not found");
}
// IDを安全に取得する
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
$file = $this->mdb2->queryRow("SELECT * FROM uploaded_files WHERE id = ". $this->mdb2->quote($id, 'integer'));
if (!$file) {
$_SESSION['flash_message'] = '指定されたファイルが見付かりませんでした';
header('Location: index.php');
}
// ファイルをファイルシステムから削除
$filePath = $this->config->get(null, 'upload_dir') . $file['saved_name'];
if (file_exists($filePath)) {
unlink($filePath);
}
// データベースからファイル情報を削除
$sql = "DELETE FROM uploaded_files WHERE id = ". $this->mdb2->quote($id, 'integer');
$result = $this->mdb2->query($sql);
if (PEAR::isError($result)) {
$this->mdb2->rollback(); // エラーがあればロールバック
$this->logger->err("ファイル削除に失敗しました: " . $result->getDebugInfo());
throw new Exception($result->getDebugInfo());
}
$this->logger->info('ID: '.$id.' ファイル削除処理が正常に終了しました。');
$_SESSION['flash_message'] = 'ファイルを正常に削除しました。';
header('Location: index.php');
exit;
}
public function execute()
{
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
case 'save':
$this->save();
break;
case 'delete':
$this->delete();
break;
default:
$this->display();
}
}
}
$uploader = new SimpleUploader();
$uploader->execute();
何のためにこれを書いたのか
→ 別の言語やフレームワークへポーティング(移植)も課題として使えるだろうが
以下はchatgptによる提案
ただここでの目的はポーティングではなく、この旧世代のアプリをECS Fargateで動作させるということをやってみる。次回からはプログラミング講座ではないので敷居は一気に上がりますよ〜(何でもできないと今日日食えないかもしれんよw)
この記事が気に入ったらサポートをしてみませんか?