見出し画像

今時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で動かすってところまでやるからね。







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