見出し画像

今時php5でノーフレームワークでアップローダーを作る(2) ー もうちょい調整する

まず、ログを適当に出力したい。まあ、あとはデモなのでどうでもいいっちゃどうでもいい。classにしてもいいっちゃいい。初心者向け的な解説も含めて後でするか。最近じゃ1からclassを使ったアプリの設計ってやらないんじゃない?フレームワーク使えばあんま必要ないし…

ちなみにだが、ここでの企みは別に今時化石みてえなphp5でプログラムを組む勉強をしたいわけじゃないから、それ自体はどうでもよかったりする。最終的にはこの古いテクノロジーのプログラムを今の環境で動作させるにはどうしたらいいのかというところに話は流れていく。だが今顧客の古いPGを見せるわけにはいかないからしょーがなくでっち上げてるだけだw

ロガーの導入

方針としてpearを使えるものは使う。pearのLogはpackagistにあるから、それを使う

% docker-compose exec web composer require pear/log

ログの場所をとりあえずconfig.ini に書いとこうか

upload_dir = 'stored/'
log_dir = 'logs/'
db_dsn = 'mysqli://user:password@mysql/simple_app?charset=utf8'

そしたら

<?php
require_once './vendor/autoload.php';

$config = new Config_Lite('config.ini');

// ロガーの設定
$logDir = $config->get(null, 'log_dir');
$logger = Log::singleton('file', $logDir. '/app.log', 'ident', array('mode' => 0600, 'timeFormat' => '%X %x'));

こんな感じでindex.php に組み込んでいる

書く

あとは適当に書く、の前に

% mkdir logs
% sudo chown www-data logs -R

とディレクトリを準備しつつ

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
    $logger->info('ファイルアップロード処理が開始されました。');
// <snip...>
    $logger->info('ファイルアップロード処理が正常に終了しました。');
    $_SESSION['flash_message'] = 'ファイルが正常にアップロードされました。';
    header('Location: index.php');
    exit;
}

とすると

03:27:01 02/01/24 ident [info] ファイルアップロード処理が開始されました。
03:27:01 02/01/24 ident [info] ファイルアップロード処理が正常に終了しました。

こんなような内容がファイルlogs/app.log に書かれていることだろう。こんな内容を掃き出す意味はほぼ無い。基本的にはエラーログを書く事になるんじゃないだろうか、ただ、ここでは「何かログる」というファンクションが必要なので作った。これはECSかなんかにデプロイするときにまた解説するわ。

アップロードしたファイルのあつかい

今、ファイルへのリンクが無い。まあ、本来的にはダイレクトにリンクしない方がいいのかもわからないが、ここではデモっていうのもあるし、ダイレクトにリンクしてしまおう

index.php 

// データベースからアップロードされたファイルのメタデータを取得
$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/'ディレクトリを想定
}

index.tpl 

    {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>
            {* ファイル名をリンクテキストとして、ファイルのURLをリンクのhref属性として使用 *}
            <a href="{$file.url}" target="_blank">{$file.saved_name}</a>
          </td>

削除リンクの提供

ここでは誰もがファイルを削除できるようにする。ただ、ファイルにパスワードなどかけると、こんな糞みてえなプログラムでもまあまあ使えるのかもしれないね。なおそこまでは今んところ考えてない。興味があれば改造してみたらどうかな_

viewの更新

    {if $files|@count > 0}
    <table border="1">
      <thead>
        <tr>
          <th>ファイル</th>
          <th>サイズ</th>
          <th>アップロード日時</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {foreach from=$files item=file}
        <tr>
          <td>
            {* ファイル名をリンクテキストとして、ファイルのURLをリンクのhref属性として使用 *}
            <a href="{$file.url}" target="_blank">{$file.saved_name}</a>
          </td>
          <td>{$file.size} bytes</td>
          <td>{$file.uploaded_at}</td>
          <td>
            <a href="index.php?id={$file.id}&amp;action=delete" onclick="return confirm('本当に削除しますか?');">削除</a>
          </td>
        </tr>
        {/foreach}
      </tbody>
    </table>

ここではactionというキーをクエリーストリングにして処理を分岐するようにしている。ここにdeleteが入ったときは削除とする。なおactionというキーを削除だけに使うのは超絶イマイチなので次回のリファクターで何とかしよう。

index.php 

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;

}

viewがダセエとやる気がおきねえ…


ノースタイルで旧世代感が凄い

これじゃあなあ…ちょっと改善してみようか流石に。CDNでいいや

どうせならbootstrapとかtailwindとかから離れてみよう

Bulmaをつかってみた例(っつーか全部AIが作ってるけど)

ちなみにこのBulmaって名前はドラゴンボールから来てるらしいぞwww

https://github.com/jgthms/bulma/issues/17
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>File Uploader</title>
    <!-- Bulma CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.min.css">
  </head>
  <body>
    <section class="section">
      <div class="container">
        {if $flashMessage}
        <div class="notification is-primary">
          <button class="delete"></button>
          {$flashMessage}
        </div>
        {/if}

        <div class="box">
          <h1 class="title">
            <a href="index.php">File Uploader</a>
          </h1>
          <form action="index.php" method="post" enctype="multipart/form-data">
            <div class="file has-name">
              <label class="file-label">
                <input class="file-input" type="file" name="file" required>
                <span class="file-cta">
                  <span class="file-icon">
                    <i class="fas fa-upload"></i>
                  </span>
                  <span class="file-label">Choose a file…</span>
                </span>
                <span class="file-name">No file selected</span>
              </label>
            </div>
            <button class="button is-link" type="submit">Upload</button>
          </form>
        </div>

        {if $files|@count > 0}
        <table class="table is-fullwidth is-striped">
          <thead>
            <tr>
              <th>ファイル</th>
              <th>サイズ</th>
              <th>アップロード日時</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {foreach from=$files item=file}
            <tr>
              <td><a href="{$file.url}" target="_blank">{$file.saved_name}</a></td>
              <td>{$file.size} bytes</td>
              <td>{$file.uploaded_at}</td>
              <td><a class="button is-small is-danger" href="index.php?action=delete&id={$file.id}" onclick="return confirm('本当に削除しますか?');">削除</a></td>
            </tr>
            {/foreach}
          </tbody>
        </table>
        {else}
        <div class="notification is-warning">アップロードされたファイルはありません。</div>
        {/if}
      </div>
    </section>
    <script>
document.addEventListener('DOMContentLoaded', function () {
    // ファイル入力要素を取得
    var fileInput = document.querySelector('.file-input');
    var fileLabel = document.querySelector('.file-name');

    // ファイル入力フィールドが変更されたときのイベントリスナーを設定
    fileInput.addEventListener('change', function (e) {
        // 選択されたファイルのリストを取得
        var files = e.target.files;
        if (files.length > 0) {
            // 最初のファイルの名前を取得して表示
            fileLabel.textContent = files[0].name;
        } else {
            // ファイルが選択されていない場合はデフォルトのテキストを表示
            fileLabel.textContent = 'No file selected';
        }
    });
});
    </script>

  </body>
</html>

まあ見た目だけでもモダンにするかっていうね

随分変わるやろ

optional: せっかくだしbyteサイズももうちょっと何とかしよう

% docker-compose exec web composer require gabrielelana/byte-units

すると

% docker-compose exec web composer require gabrielelana/byte-units
The "https://repo.packagist.org/packages.json" file could not be downloaded: failed to open stream: Cannot assign requested address
https://repo.packagist.org could not be fully loaded, package information was loaded from the local cache and may be out of date
Using version ^0.5.0 for gabrielelana/byte-units
./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)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for gabrielelana/byte-units ^0.5.0 -> satisfiable by gabrielelana/byte-units[0.5.0].
    - gabrielelana/byte-units 0.5.0 requires ext-bcmath * -> the requested PHP extension bcmath is missing from your system.

  To enable extensions, verify that they are enabled in your .ini files:
    -
    - /usr/local/etc/php/conf.d/docker-php-ext-mysql.ini
    - /usr/local/etc/php/conf.d/docker-php-ext-mysqli.ini
    - /usr/local/etc/php/conf.d/docker-php-timezone.ini
  You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode.

Installation failed, reverting ./composer.json to its original content.

などとbc-math extを要求されるので

docker/php/Dockerfile 

# bcmath module
RUN docker-php-ext-install bcmath
# mysql module
#RUN docker-php-ext-install mysql
RUN docker-php-ext-install mysqli

などとし

% docker-compose build --no-cache

とかしてdockerを再起動すればinstall可能だ。

% docker-compose exec web composer require gabrielelana/byte-units
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 ^0.5.0 for gabrielelana/byte-units
./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 gabrielelana/byte-units (0.5.0): Downloading (100%)
Package nanasess/mdb2_driver_mysqli is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating autoload files

そうしたらば

templates/index.tpl 

              {* <td>{$file.size} bytes</td> *}
              <td>{$file.size|formatFileSize}</td>

とする。このままだと当然

Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "file:/var/www/html/templates/index.tpl" on line 55 "<td>{$file.size|formatFileSize}</td>" unknown modifier 'formatFileSize' <-- thrown in /var/www/html/vendor/smarty/smarty/libs/sysplugins/smarty_internal_templatecompilerbase.php on line 55

となるので、index.php で加工していく

// Smartyオブジェクトの作成
$smarty = new Smarty();
use ByteUnits\Metric;

function formatFileSize($size) {
    return Metric::bytes($size)->format();
}
// Smartyにプラグインとして関数を登録
$smarty->registerPlugin('modifier', 'formatFileSize', 'formatFileSize');

とすれば

サイズが人の目に読みやすくなったやろ

となるだろう

全文

ここまでで機能の開発は一通り終了だ。全文を見ていこう。

index.php 

<?php
require_once './vendor/autoload.php';

$config = new Config_Lite('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');

config.ini 

upload_dir = 'stored/'
log_dir = 'logs/'
db_dsn = 'mysqli://user:password@mysql/simple_app?charset=utf8'

templates/index.tpl 

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>File Uploader</title>
    <!-- Bulma CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.min.css">
  </head>
  <body>
    <section class="section">
      <div class="container">
        {if $flashMessage}
        <div class="notification is-primary">
          <button class="delete"></button>
          {$flashMessage}
        </div>
        {/if}

        <div class="box">
          <h1 class="title">
            <a href="index.php">File Uploader</a>
          </h1>
          <form action="index.php" method="post" enctype="multipart/form-data">
            <div class="file has-name">
              <label class="file-label">
                <input class="file-input" type="file" name="file" required>
                <span class="file-cta">
                  <span class="file-icon">
                    <i class="fas fa-upload"></i>
                  </span>
                  <span class="file-label">Choose a file…</span>
                </span>
                <span class="file-name">No file selected</span>
              </label>
            </div>
            <button class="button is-link" type="submit">Upload</button>
          </form>
        </div>

        {if $files|@count > 0}
        <table class="table is-fullwidth is-striped">
          <thead>
            <tr>
              <th>ファイル</th>
              <th>サイズ</th>
              <th>アップロード日時</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {foreach from=$files item=file}
            <tr>
              <td><a href="{$file.url}" target="_blank">{$file.saved_name}</a></td>
              {* <td>{$file.size} bytes</td> *}
              <td>{$file.size|formatFileSize}</td>
              <td>{$file.uploaded_at}</td>
              <td><a class="button is-small is-danger" href="index.php?action=delete&id={$file.id}" onclick="return confirm('本当に削除しますか?');">削除</a></td>
            </tr>
            {/foreach}
          </tbody>
        </table>
        {else}
        <div class="notification is-warning">アップロードされたファイルはありません。</div>
        {/if}
      </div>
    </section>
    <script>
document.addEventListener('DOMContentLoaded', function () {
    // ファイル入力要素を取得
    var fileInput = document.querySelector('.file-input');
    var fileLabel = document.querySelector('.file-name');

    // ファイル入力フィールドが変更されたときのイベントリスナーを設定
    fileInput.addEventListener('change', function (e) {
        // 選択されたファイルのリストを取得
        var files = e.target.files;
        if (files.length > 0) {
            // 最初のファイルの名前を取得して表示
            fileLabel.textContent = files[0].name;
        } else {
            // ファイルが選択されていない場合はデフォルトのテキストを表示
            fileLabel.textContent = 'No file selected';
        }
    });
});
    </script>

  </body>
</html>

となる。まバグってるかもしれんけど、ここまでの状態をgitlabに置いといたからややこしい人はcloneしてください。

git clone https://gitlab.com/catatsumuri/old-style-php5-uploader.git

てかconfig.iniをwebrootにベタっとおくのはアレなんでちょっと変えとります

この記事が気に入ったらサポートをしてみませんか?