見出し画像

php5アップローダーをECSに配置する(3) - 環境変数でDB接続する +CloudWatch Logs

環境変数からDBに接続できるよう加工する

前回、こんなコードを書いた

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

// 環境変数の取得
$app_env = getenv('APP_ENV');
$db_user = getenv('DB_USER');
$db_pass = getenv('DB_PASSWD');
$db_host = getenv('DB_HOST');
$db_name = getenv('DB_NAME');

// 接続情報のダンプ(デバッグ情報)
r([
  'APP_ENV' => $app_env,
  'DB_USER' => $db_user,
  'DB_PASSWD' => $db_pass,
  'DB_HOST' => $db_host,
  'DB_NAME' => $db_name
]);


$dsn = 'mysqli://admin:password@uploader-demo.cmxstlofflyf.ap-northeast-1.rds.amazonaws.com/uploader_demo?charset=utf8';

$mdb2 = MDB2::connect($dsn);
if (PEAR::isError($mdb2)) {
    r($mdb2->getDebugInfo());
    exit;
}

$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
r($mdb2->queryAll("SELECT * from uploaded_files"));

これの実行結果は

こんなのであったわけだが、これを利用してDBを接続できるよう加工していく。まずはcheckdb.phpでやってみよう。

MDB2::parseDsn()

いや、今時mdb2の事をやってもしゃーねえだろって本当にその通りなんだけどまあこういった部分はある程度やんねえとしゃーないってところもあるし…ってことでMDB2のparseDsnはまあ要するに

$dsn = 'mysqli://admin:password@uploader-demo.cmxstlofflyf.ap-northeast-1.rds.amazonaws.com/uploader_demo?charset=utf8';

みたいな文字列を配列にしてくれるって感じだ。従ってバーっとコードを書いてみると

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

// 環境変数の取得
$app_env = getenv('APP_ENV');
$db_user = getenv('DB_USER');
$db_pass = getenv('DB_PASSWD');
$db_host = getenv('DB_HOST');
$db_name = getenv('DB_NAME');

// 接続情報のダンプ(デバッグ情報)
r([
  'APP_ENV' => $app_env,
  'DB_USER' => $db_user,
  'DB_PASSWD' => $db_pass,
  'DB_HOST' => $db_host,
  'DB_NAME' => $db_name
]);

$this->config = new Config_Lite(__DIR__ . '/config/config.ini');
// $dsn = 'mysqli://admin:password@uploader-demo.cmxstlofflyf.ap-northeast-1.rds.amazonaws.com/uploader_demo?charset=utf8';
$dsn = $this->config->get(null, 'db_dsn');

$parsedDsn = MDB2::parseDsn($dsn);
r($parsedDsn);
$parsedDsn['username'] = getenv('DB_USER')   ?: $parsedDsn['username'];
$parsedDsn['password'] = getenv('DB_PASSWD') ?: $parsedDsn['password'];
$parsedDsn['hostspec'] = getenv('DB_HOST')   ?: $parsedDsn['hostspec'];
$parsedDsn['database'] = getenv('DB_NAME')   ?: $parsedDsn['database'];
r($parsedDsn);

こんん感じである。ここではr() 関数を3つ出している。1つは環境変数のdump。その次はconfigから得られたdsnのdump。最後に環境変数で上書きしたdsnのdumpである。

hostspecの違いに注目

まあもろもろ書き変わってはいるんだが、とくにこのhostspecのところがちゃんと上書きされている事を確認しときたいよな。そしたら、これで接続してみよう。

って、おおっと、dbとhost名をそういや変更したのを忘れて古いまんまのスクショになってたぜ…環境変数を直しましたとさ。

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

// 環境変数の取得
$app_env = getenv('APP_ENV');
$db_user = getenv('DB_USER');
$db_pass = getenv('DB_PASSWD');
$db_host = getenv('DB_HOST');
$db_name = getenv('DB_NAME');

// 接続情報のダンプ(デバッグ情報)
/*
r([
  'APP_ENV' => $app_env,
  'DB_USER' => $db_user,
  'DB_PASSWD' => $db_pass,
  'DB_HOST' => $db_host,
  'DB_NAME' => $db_name
]);
 */

$config = new Config_Lite(__DIR__ . '/config/config.ini');
// $dsn = 'mysqli://admin:password@uploader-demo.cmxstlofflyf.ap-northeast-1.rds.amazonaws.com/uploader_demo?charset=utf8';
$dsn = $config->get(null, 'db_dsn');

$parsedDsn = MDB2::parseDsn($dsn);
$parsedDsn['username'] = getenv('DB_USER')   ?: $parsedDsn['username'];
$parsedDsn['password'] = getenv('DB_PASSWD') ?: $parsedDsn['password'];
$parsedDsn['hostspec'] = getenv('DB_HOST')   ?: $parsedDsn['hostspec'];
$parsedDsn['database'] = getenv('DB_NAME')   ?: $parsedDsn['database'];

r($parsedDsn);

$mdb2 = MDB2::connect($parsedDsn);
if (PEAR::isError($mdb2)) {
    r($mdb2->getDebugInfo());
    exit;
}
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
r($mdb2->queryAll("SELECT * from uploaded_files"));

実行結果

とまあこのように接続が成功し、array()が帰ってきている。まあ今は何も入ってないのでこれで正解だ。

ログ

まあここではあえて失敗するとかもするけど、どういう形でロギングがなされるか見ていこう。

ProductionとDeveloppmentの切り替え

ここでは APP_ENV でproductionモードとdevelopmodeを切り替えるとする。ここではコンストラクターを変更している。開発ラインも引き続き動いているのもまた重要である。

    public function __construct()
    {
        $this->config = new Config_Lite(__DIR__ . '/config/config.ini');
        // 環境変数をチェックし、ログ設定を行う
        $env = getenv('APP_ENV') ?: 'development';
        if ($env === 'development') {
            // 開発環境用の設定
            $logDir = $this->config->get(null, 'log_dir');
            $this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
                'mode' => 0600,
                'timeFormat' => '%X %x'
            ]);
        } else {
            // 本番環境用の設定
            ini_set('display_errors', '0');
            $this->logger = Log::singleton('error_log', PEAR_LOG_TYPE_SYSTEM, 'ident');
        }

        // 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("DB connect failed: " . $this->mdb2->getDebugInfo());
            throw new Exception("データーベースに障害が発生しています");
        }
        $this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
    }

で、これこの部分

        if ($env === 'development') {
            // 開発環境用の設定
            $logDir = $this->config->get(null, 'log_dir');
            $this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
                'mode' => 0600,
                'timeFormat' => '%X %x'
            ]);
        } else {
            // 本番環境用の設定
            ini_set('display_errors', '0');
            $this->logger = Log::singleton('error_log', PEAR_LOG_TYPE_SYSTEM, 'ident');
        }

本番環境はlogs/app.log とかに落としてるんだけど、ECSの場合はそれやってもタスクが終わったら揮発しちゃって意味ないのでcloudwatchに送りこむというか、基本的にはstrerr(標準エラー出力)に送ってしまう。そうすればまあ何とかはなる。

ってわけでここでいろいろログを送信して見てみよう。あとの処理は止めちゃっていい。

そうすると出力は 

r($env);

この部分しかないので

$envの部分のみブラウザーには出てきている

このようになっているが、ではログはどうだろうか。

このようにタスクのログを見ても出てないときがある。これはまあこういうもんってことでCloudWatchで表示してみよう

ここでテーリングを開始し、そのあとで何度かリロードしてみると

まあこんな風になってんだけど日本語の部分がアレな感じでアレである。

ただmonologに差し替えると

こんな感じになるから、今時Pear LOGとか使ってる場合じゃねえぞって感じもあるな。php5.6でも動作するため。

これらを踏まえてindex.phpを修正していく

テスト書いてない状態でリファクタリングはなかなか緊張するぜ…

ロガーの問題

<?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);
        $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();
    }

    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();

とりあえず全文。PEAR Logを捨てる改造を行う。コンストラクター外からインスタンス化してclassにつっこむ手もあるが、まあそれはよしとしよう。

<?php
session_start();
require_once './vendor/autoload.php';
use ByteUnits\Metric;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

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'
        // ]);
        // Monologインスタンスの初期化
        $this->logger = new Logger('SimpleUploader');
        $this->logger->pushHandler(new StreamHandler($logDir . '/app.log', Logger::DEBUG));

このようにPEAR Logからmonologに差し替えている。ここで$this->loggerを差してるところを全部検索していくと

        if (PEAR::isError($this->mdb2)) {
            $this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
            throw new Exception("データベース接続に失敗しました");
        }
        if (PEAR::isError($result)) {
            $this->mdb2->rollback(); // エラーがあればロールバック
            $this->logger->err("更新処理に失敗しました: " . $result->getDebugInfo());
            throw new Exception($result->getDebugInfo());
        }
        if (PEAR::isError($updateResult)) {
            $this->mdb2->rollback(); // エラーがあればロールバック
            $this->logger->err("保存ファイル名の処理に失敗しました: " . $updateResult->getDebugInfo());
            throw new Exception($updateResult->getDebugInfo());
        }
        $this->logger->info('ファイルアップロード処理が正常に終了しました。');
        if (PEAR::isError($result)) {
            $this->mdb2->rollback(); // エラーがあればロールバック
            $this->logger->err("ファイル削除に失敗しました: " . $result->getDebugInfo());
            throw new Exception($result->getDebugInfo());
        }
        $this->logger->info('ID: '.$id.' ファイル削除処理が正常に終了しました。');

まあ基本的にerr()error()に置換すれば何とかなるなる

+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;

 class SimpleUploader
 {
@@ -14,10 +16,16 @@ class SimpleUploader
     {
         $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'
-        ]);
+
+        // $this->logger = Log::singleton('file', $logDir . '/app.log', 'ident', [
+        //    'mode' => 0600,
+        //    'timeFormat' => '%X %x'
+        // ]);
+        // Monologインスタンスの初期化
+        $this->logger = new Logger('SimpleUploader');
+        $this->logger->pushHandler(new StreamHandler($logDir . '/app.log', Logger::DEBUG));
+
+
         // Smartyインスタンスの初期化
         $this->smarty = new Smarty();
         $this->smarty->template_dir = 'templates';
@@ -29,7 +37,7 @@ class SimpleUploader
         $this->mdb2 = MDB2::connect($dsn);
         $this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
         if (PEAR::isError($this->mdb2)) {
-            $this->logger->err("データベース接続に失敗しました: " . $this->mdb2->getMessage());
+            $this->logger->error("データベース接続に失敗しました: " . $this->mdb2->getMessage());
             throw new Exception("データベース接続に失敗しました");
         }
     }

productionによるloggerの切り替え

    public function __construct()
    {
        $this->config = new Config_Lite(__DIR__ . '/config/config.ini');

        // 環境変数をチェックし、ログ設定を行う
        $env = getenv('APP_ENV') ?: 'development';
        $this->logger = new Logger('SimpleUploader');

        if ($env === 'production') {
            // 本番環境では標準エラー出力にログを送る
            $this->logger->pushHandler(new StreamHandler('php://stderr', Logger::WARNING));
        } else {
            // 開発環境ではファイルにログを記録
            $logDir = $this->config->get(null, 'log_dir');
            $this->logger->pushHandler(new StreamHandler($logDir . '/app.log', Logger::DEBUG));
        }

そしてDBの接続を環境変数から行うコード

    public function __construct()
    {
        $this->config = new Config_Lite(__DIR__ . '/config/config.ini');

        // 環境変数をチェックし、ログ設定を行う
        $env = getenv('APP_ENV') ?: 'development';
        $this->logger = new Logger('SimpleUploader');

        if ($env === 'production') {
            // 本番環境では標準エラー出力にログを送る
            $this->logger->pushHandler(new StreamHandler('php://stderr', Logger::WARNING));
        } else {
            // 開発環境ではファイルにログを記録
            $logDir = $this->config->get(null, 'log_dir');
            $this->logger->pushHandler(new StreamHandler($logDir . '/app.log', Logger::DEBUG));
        }

        // 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');
        $parsedDsn = MDB2::parseDsn($dsn);
        // 環境変数から値を取得し、上書き
        $parsedDsn['username'] = getenv('DB_USER')   ?: $parsedDsn['username'];
        $parsedDsn['password'] = getenv('DB_PASSWD') ?: $parsedDsn['password'];
        $parsedDsn['hostspec'] = getenv('DB_HOST')   ?: $parsedDsn['hostspec'];
        $parsedDsn['database'] = getenv('DB_NAME')   ?: $parsedDsn['database'];

        $this->mdb2 = MDB2::connect($parsedDsn);
        if (PEAR::isError($this->mdb2)) {
            $this->logger->err("DB connect failed: " . $this->mdb2->getDebugInfo());
            throw new Exception("データーベースに障害が発生しています");
        }
        $this->mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);
    }

このコンストラクターの設定で動作するはずだ。


ただし、uploadは失敗すると思う

これはファイル保存部分の設定をまともにやっていないからであり、この部分はこの部分でまた考慮しないといけないところであるな。

ってわけで次回は

ファイルの保存エリアをまともに考えてみよう。次回からタスクを2つ動作させる事も視野に入れていくぞい。

あと最後はsessionと負荷試験とオートスケールかな…求められるスキルセットの幅が広うございますなあ。。遊んでる暇なんてあんのか?




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