🆙

PHPでのFile Upload のやりかた

2024/12/22に公開

今はやり方をしっかり知らなくてもCMSとかFrameworkのライブラリで簡単にできちゃうのがUpload。
Uploadの方法を書いて!ってAIに言ったらできちゃうしね。
でも、これをあえてPUREなPHPで書いて仕組みをちゃんと知りたいねと。
基本の基本を書いていこうと思います。

まずはPHPの設定から

php.iniというPHPの設定ファイルが存在します。
その中でPHPのアップロードが許可されているかを確認します。
また、アップロードが許可されていても、ファイルサイズ・アップロード時間の指定もあります。
その辺をしっかり確認してみましょう。

php.ini

// アップロードの許可
file_uploads = On

// アップロードできる最大サイズ
upload_max_filesize = 128M

// postファイルの最大サイズ
post_max_size = 128M

// メモリサイズ
memory_limit = 128M

//タイムアウト時間
max_execution_time = 360

よく、PHPMyAdminなどでDBがインポートできないときの原因は大概この辺の設定にあります。
自分の所感としては、memory_limit=>post_max_size=>upload_max_filesize です。
全部同じでも問題ないです。必要な数値を設定します。

処理中に止まらないように、タイムアウト時間も余裕を持たせて設定します。

ファイルアップロード用のフォームを作成

ファイルアップロードのformタグを書いた、index.php、post先にprocess.php、成功した画面用にsuccess.phpを作成します。

index.php
<?php
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="w-full max-w-md">
        <form action="process.php" method="POST" enctype="multipart/form-data" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
            <h2 class="text-2xl font-bold mb-6 text-center text-gray-700">ファイルアップロード</h2>
            <div class="mb-6">
                <label for="up-file" class="block text-gray-700 text-sm font-bold mb-2"> アップロードするファイル:</label>
                <input type="file"  name="up-file"  id="up-file"
                       class="block w-full text-sm text-gray-500
                              file:mr-4 file:py-2 file:px-4
                              file:rounded-md file:border-0
                              file:text-sm file:font-semibold
                              file:bg-blue-50 file:text-blue-700
                              hover:file:bg-blue-100">
            </div>
            <div class="flex items-center justify-center">
                <button type="submit" 
                        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out">アップロード</button>
            </div>
        </form>
    </div>
</body>
</html>

こんな感じの画面になります。(見た目を整えるのにTailwindcssを使っています)

大事なのは、inputタグのenctype="multipart/form-data"。これが無いとファイルのアップロードができません。
あとファイルアップロードの時は、inputタグのtypeはfileになります。

success.php
<?php
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>処理完了</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
    <div class="container mx-auto px-4 py-16">
        <div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-8">
            <!-- 成功アイコン -->
            <div class="flex justify-center mb-6">
                <div class="bg-green-100 rounded-full p-3">
                    <svg class="w-12 h-12 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
                    </svg>
                </div>
            </div>
            
            <h1 class="text-3xl font-bold text-center text-gray-800 mb-4">処理が完了しました</h1>
            
            <div class="text-center mb-8">
                <p class="text-gray-600">
                    <?php echo htmlspecialchars($success_message); ?>
                </p>
            </div>
            
            <div class="text-center">
                <a href="index.php" 
                   class="inline-block bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-6 rounded-lg transition duration-300">
                    トップページに戻る
                </a>
            </div>
        </div>
    </div>
</body>
</html>

success.phpは成功時に$success_messageを受け取って表示する仕様にしておきます。

Sessionを使って便利にしちゃおう

セッションを使ってメッセージやトークンの設定ができるようにします。
全てのphpファイルの先頭に追加します。

//session start
session_start();

index.phpではフォームのトークンの生成をしたいので、index.phpに以下を追加します。

index.php
<?php
session_start();
//token create
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
?>
...(略)...
<form>
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
    <input type="hidden" name="action" value="uploadFile">

これでPOSTの際に、トークンが一致すると処理が行われるフォームになります。
安全なフォームには必要な処理です。

POST先のprocess.phpの処理

index.phpからポストされたファイルの処理を行います。
まずはリクエストの確認から、トークンの検証。それが終わるとようやく、ポストデータの処理になります。

ポストデータの処理は、アップロード先の取得 → エラーチェック → 拡張子チェック → ファイル名変更の順になります。
全て成功すると、successに渡すセッションを代入してリダイレクトします。

process.php
<?php
/**
 * post process
 */
session_start();

 // POSTリクエストの確認
if  (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'uploadFile') {
    // POSTデータの取得と検証
    $data = array_map(function($value) {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    }, $_POST);

    // CSRFトークンの検証
    if (!isset($data['csrf_token']) || !validateCsrfToken($data['csrf_token'])) {
        die('不正なリクエストです');
    }

    //アップロードを許可する拡張子
    $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'webp'];

    try {
        // ここでPOSTデータの処理を行う

        // Upload Dir
        $UpDir = __DIR__ . '/uploads/';
        // ディレクトリが存在しない場合は作成
        if (!file_exists($UpDir)) {
            mkdir($UpDir, 0777, true);
        }


        if (
            !isset($_FILES['up-file']['error']) ||
            is_array($_FILES['up-file']['error'])
        ) {
            throw new RuntimeException('無効なパラメータです。');
        }

        //error check
        switch ($_FILES['up-file']['error']) {
            case UPLOAD_ERR_OK:
                break;
            case UPLOAD_ERR_NO_FILE:
                throw new RuntimeException('ファイルは送信されませんでした。');
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                throw new RuntimeException('ファイルサイズの制限を超えました。');
            default:
                throw new RuntimeException('不明なエラー。');
        }

        // You should also check filesize here. (128MB = 134217728B )
        //PHPの設定ではなくサイトで5MBにしたなら5242880になる
        if ($_FILES['up-file']['size'] > 134217728) {
            throw new RuntimeException('ファイルサイズの制限を超えました。');
        }

        //拡張子チェック
        $finfo = pathinfo($_FILES['up-file']['name']);
        $ext = strtolower($finfo['extension']);
        if (in_array($ext, $allowedExtensions) === false) {
            throw new RuntimeException('ファイル形式が無効です。');
        }

        //ファイル名の変更をするとき
        //日本語ファイル名を日時に変えるならsha1_file($_FILES['up-file']['tmp_name'])をtime()などに
        $newFileName = sprintf( $UpDir . '%s.%s',
            sha1_file($_FILES['up-file']['tmp_name']),
            $ext
        );
        if (!move_uploaded_file( $_FILES['up-file']['tmp_name'], $newFileName)) {
            throw new RuntimeException('アップロードしたファイルの移動に失敗しました。');
        }

        // セッションに成功メッセージを保存
        $_SESSION['success_message'] = 'ファイルのアップロードが完了しました。';


        // 処理成功時のリダイレクト
        header('Location: success.php');
        exit;

    } catch (Exception $e) {
        // エラー処理
        $error = $e->getMessage();
    }
}

function validateCsrfToken($token) {
    return hash_equals($_SESSION['csrf_token'], $token);
}

indexとsuccessにsession追加

proccessでエラーになったときの戻り先がindexです。
セッションエラーを記述できるようにコードを追加します。

index.php
<?php
//session start
session_start();

//token create
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];

// エラーメッセージを取得して、セッションから削除
$error = $_SESSION['error'] ?? null;
$success = $_SESSION['success'] ?? null;
unset($_SESSION['error']);
unset($_SESSION['success']);
?>

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="w-full max-w-md">
        <?php if ($error): ?>
            <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
                <span class="block sm:inline"><?= htmlspecialchars($error) ?></span>
            </div>
        <?php endif; ?>
        
        <?php if ($success): ?>
            <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert">
                <span class="block sm:inline"><?= htmlspecialchars($success) ?></span>
            </div>
        <?php endif; ?>

success.phpはSUCCESSメッセージを受け取れるように追加します。

success.php
session_start();

// セッションに成功メッセージがない場合はホームページにリダイレクト
if (!isset($_SESSION['success_message'])) {
    header('Location: index.php');
    exit();
}

// 成功メッセージを取得して、セッションから削除
$success_message = $_SESSION['success_message'];
unset($_SESSION['success_message']);
?>

<!DOCTYPE html>
...

できあがり

https://youtu.be/qDKTrj90MWo

こんな感じになります。
あとはご自身の環境に合わせて、ImageMagicで適時ファイルの整形をしたりすると良いかと思います。

完成形のファイルは、Githubに上げておきますので必要であればDLどうぞ。

https://github.com/369work/file-upload

Discussion