LoginSignup
45
55

More than 3 years have passed since last update.

PHPによる(複数)画像アップロード処理

Last updated at Posted at 2020-09-27

PHPの画像処理は苦手とする方が多いみたいで、リクエストをいただいたので、解説していきます!
他にもリクエストがあったら是非お願いします!
期待にお応えできるかはわからないですが笑

フロントエンド側の処理は簡単なので省略します笑
一応補足しておくと
post送信でenctype='multipart/form-data'これを指定するだけです!

user_edit.php
<?php session_start(); ?>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>ユーザー情報編集</title>
</head>
<body>
<h1>ユーザー情報編集</h1>
<form method='post' enctype='multipart/form-data' action='upload.php'>
<input type='hidden' name='member_id' value='<?= $_GET['member_id']; ?>'>
<p>アップロード画像</p>
<input type='file' name='photo'>
<input type='submit' name='upload' value='アップロード'>
</form>
<form method='post' action='user_detail.php'>
<p>一言メッセージ<p>
<input type='hidden' name='member_id' value='<?= $_GET['member_id']; ?>'>
<input type='text' name='message' value='<?php if ($_GET['message'] != '未入力') echo $_GET['message']; ?>'>
<input type='submit' name='submit' value='送信'>
</form>
<?php endif; ?>
</body>
</html>
upload.php
<?php
session_start();
if (isset($_POST['upload']) && isset($_FILES['photo']) && isset($_SESSION['id']) && isset($_POST['member_id']) && $_SESSION['id'] == $_POST['member_id']) {
    if (empty($_FILES['photo']['name'])) {
        $msg = 'ファイルを入れてください';
    } else {
        $photo = Date('Ymdhis');
        $photo .= uniqid(mt_rand(), true);
        $tempfile = $_FILES['photo']['tmp_name'];
        switch (@exif_imagetype($tempfile)) {
            case 1:
                $photo .= '.gif';
                break;
            case 2:
                $photo .= '.jpg';
                break;
            case 3:
                $photo .= '.png';
                break;
            default:
                header('Location: upload.php');
                exit();
        }
        $filemove = './photo/' . $photo;
        try {
            $dbh = new PDO('mysql:host=localhost; dbname=dbname;', 'username', 'password');
        } catch (PDOException $e) {
            echo '接続失敗:' . $e->getMesssage();
            exit();
        }
        $upload = 'UPDATE members SET photo = :photo WHERE id = :id';
        $stmt = $dbh->prepare($upload);
        $params = array(
            ':photo' => $photo,
            ':id' => $_SESSION['id'],
        );
        if (move_uploaded_file($tempfile, $filemove)) {
            $msg = '画像アップロード完了';
            $stmt->execute($params);
        } else {
            $msg = '画像ファイルをアップロードできませんでした';
        }
    }
} else {
    $msg = '画像ファイルを入れてください';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>画像アップロード</title>
</head>
<body>
<?php echo $msg; ?>
<p><a href='board.php'>ホームに戻る</a></p>
</body>
</html>

処理の流れはざっとこんな感じ

  1. アップロード写真が存在するかどうか
  2. 存在した場合は拡張子のチェックをする
  3. ユニークな名前に変更して、
  4. ファイル名をDBに保存し、
  5. ファイルを指定のディレクトリに保存する

こんな感じ!

まず最初から作るときはバリデーションはガン無視でいいです。
とりあえず最低限動くように作ってデバッグしながら追加していくのがおすすめー
自分でテストするときは悪意のある操作はしないのでね笑

では一つずつ解説していきましょう
まずアップロードされたファイルは$_FILESというグローバル変数に格納されて、サーバーに仮アップロードされます上で一時的に保存される

連想配列の中身 解説
name inputのname属性
type ファイル形式が保存されるが、ユーザー任意のMINEタイプになるので、多分$_SERVERと同じ感じで信用できない
tmp_name サーバーへ仮アップロードされたディレクトリとファイル名
error エラー情報
size ファイルサイズ(単位はバイト)

まず拡張子を確認するために、$_FILES['input属性']['tmp_name']を変数に格納して
exif_imagetypeを使って拡張子を判断していきます
exif_imagetypeが返す値は

定数
1 IMAGETYPE_GIF
2 IMAGETYPE_JPEG
3 IMAGETYPE_PNG
4 IMAGETYPE_SWF
5 IMAGETYPE_PSD
6 IMAGETYPE_BMP
7 IMAGETYPE_TIFF_II (intel byte order)
8 IMAGETYPE_TIFF_MM (motorola byte order)
9 IMAGETYPE_JPC
10 IMAGETYPE_JP2
11 IMAGETYPE_JPX
12 IMAGETYPE_JB2
13 IMAGETYPE_SWC
14 IMAGETYPE_IFF
15 IMAGETYPE_WBMP
16 IMAGETYPE_XBM
17 IMAGETYPE_ICO
18 IMAGETYPE_WEBP

以上のような値を返すので、それをswitch文で判別しているだけです

switch (@exif_imagetype($tempfile)) {
    case 1:
        $photo .= '.gif';
        break;
    case 2:
        $photo .= '.jpg';
        break;
    case 3:
        $photo .= '.png';
        break;
    default:
        header('Location: upload.php');
        exit();
}

これは以下のようにも書き換えられます

switch (@exif_imagetype($tempfile)) {
    case IMAGETYPE_GIF:
        $photo .= '.gif';
        break;
    case IMAGETYPE_JPEG:
        $photo .= '.jpg';
        break;
    case IMAGETYPE_PNG:
        $photo .= '.png';
        break;
    default:
        header('Location: upload.php');
        exit();
}

このように.=をつけると文字列の末尾に文字列を追加することができるので結果として拡張子をアップデートすることができますねー
拡張子がこの3タイプ以外の場合はリダイレクトされるようになってます

ここで@を付けているのはテキストファイルの拡張子を.jpgにするとWarning級のエラーが発生するのでそれを表示させないためです
エラー制御演算子@で画像の形式を判別するために必要なだけのバイト数を読み込めない場合にエラーを発生するのを抑制してます
この前にファイルサイズでバリデーションをかけるのもいいと思います
やろうと思えばどこまででもできてしまうのでもっと詳しく知りたい方はこちらも参考にしてみてください
https://qiita.com/mpyw/items/939964377766a54d4682

拡張子の判断が済んだら、DBにファイル名を保存していきましょう
DBに保存する際には
ファイル名を保存する方法と、
ファイルをそのままDBに保存する方法があります。
ファイルをそのままDBに保存する方法をとってしまうと重たくなってしまうので、ファイル名をDBに保存する方法をとります
詳しくは以下の記事を参考にしてください
https://qiita.com/7coco/items/1706a561131363d2c418

DBの接続は負荷がかかるため最低限にしたいのでここで接続しています
PDOには例外が発生するので、try, catchを使ってエラーハンドリングしています
ここら辺は以下の記事を参考にするといいと思います
https://qiita.com/mpyw/items/b00b72c5c95aac573b71
https://qiita.com/7968/items/6f089fec8dde676abb5b

ユーザーが入力した情報はエスケープする必要があるので、プリペアドステートメントを使ってください
注意して欲しいのはexecute()しているのはファイルを移動したのが確認できてからですかね。

move_uploaded_file()を使って、一時的に保存されたファイルを自分が保存したいディレクトリに移します

ユーザー任意のファイル名を使用するとバグを生む可能性があるのでuniqid(mt_rand(), true)を使ってランダムな文字列でファイル名を作成します
date()関数を使っているのはファイル名の重複を防止するためとデバッグ時の判別をしやすくするためです。
これには以前解説したことのあるmd5(uniqid(mt_rand(), true))を使ってもいいし、user_idをファイル名にすることもあります
そちらの解説はこちらから
https://qiita.com/satorunooshie/items/de494a25e7da4da16dcf

書き忘れていましたが移動先のディレクトリの権限を変更しておいてください!

chmod 755 ./photo

詳しくは以下のサイトを参考にしてください
https://qiita.com/shisama/items/5f4c4fa768642aad9e06
https://www.php.net/manual/ja/function.chmod.php

では続いて複数ファイルをアップロードする方法についてみていきましょう
今回は少し違うアプローチで崩していきましょう

example.php
<?php
$dir = __DIR__ . './photo';
const MAX_SIZE = 102400;
//php.iniのpost_max_sizeを超えたデータが送信されていないか確認
if (!checkPostMaxSize()) echo 'ファイルサイズは100MB以下にしてください';
if (isset($_FILES['uploadfile']) {
    for ($i = 0; $i < count($_FILES['uploadfile']['name']); $i++) {
        list($result, $ext, $error_msg) = checkFile($i);
        if ($result) {
            $name = $_FILES['uploadfile']['name'][$i];
            $tmp_name = $_FILES['uploadfile']['tmp_name'][$i];
            //"upfile_" + 現在のタイムスタンプ + 連番 + "_" + マイクロ秒 + 元のファイル名 + アクセス元IPアドレスに基づくMD5 + 拡張子
            $move_to = $dir . '/upfile_' . time() . $i . '_'
                . md5(microtime() . $name . $_SERVER['REMOTE_ADDR']) . '.' . $ext;
            if (!move_upload_file($tmp_name, $move_to)) {
                $error_msg[] = '画像のアップロードに失敗しました';
            } else {
                echo '<img src="' . $move_to . '>';
            }
        }
        if (count($error_msg) > 0) {
            foreach ($error_msg as $msg) {
                echo $msg;
            }
        }
    }
}

function checkFile($i)
{
    $error_msg = [];
    $ext = '';
    $size = $_FILES['uploadfile']['size'][$i];
    $error = $_FILES['uploadfile']['error'][$i];
    $img_type = $_FILES['uploadfile']['type'][$i];
    $tmp_name = $_FILES['uploadfile']['tmp_name'][$i];
    if ($error != UPLOAD_ERR_OK) {
        switch ($error) {
            case UPLOAD_ERR_NO_FILE:
                //アップロードされなかった場合はエラー処理はなし
                break;
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                $error_msg = 'ファイルサイズは100KB以下にしてください';
                break;
            default:
                $error_msg = 'アップロードエラーです';
        }
        return [false, $ext, $error_msg];
    } else {
        switch ($img_type) {
            case 'image/gif':
                $ext = 'gif';
                break;
            case 'image/jpeg':
            case 'image/pjpeg':
                $ext = 'jpg';
                break;
            case 'image/png':
            case 'image/x-png':
                $ext = 'png';
                break;
        }
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $finfo_type = $finfo->file($tmp_name);
        if ($size == 0) {
            $error_msg[] = 'ファイルが存在しないか空のファイルです';
        } elseif ($size > MAX_SIZE) {
            $error_msg[] = 'ファイルサイズは100KB以下にしてください';
        } elseif ($img_type != $finfo_type) {
            $error_msg[] = 'MIMEタイプが一致しません';
        } elseif ($ext != 'gif' && $ext != 'jpg' && $ext != 'png') {
            $error_msg[] = 'アップロード可能なファイルはgif, jpg, pngのみです';
        } else {
            return [true, $ext, $error_msg];
        }
    }
    return [false, $ext, $error_msg];
}
//php.iniのpost_max_sizeを超えたデータが送信されていないかチェックする関数
function checkPostMaxSize()
{
    $max_size = ini_get('post_max_size');
    //post_max_sizeにMが入っていた場合に整数にする
    $unit = substr($max_size, -1);
    switch ($unit) {
        case 'M':
            $multiple = 1024 ** 2; //pow(1024, 2);
            break;
        case 'K':
            $multiple = 1024;
            break;
        case 'G':
            $multiple = 1024 ** 3; //pow(1024, 3);
            break;
        default:
            $multiple = 1;
    }
    $max_size = substr($max_size, 0, strlen($max_size) - 1) * $multiple;
    //post_max_sizeを超えたデータがPOSTされたかをチェック
    if ($_SERVER['REQUEST_METHOD'] == 'POST'
        && $_SERVER['CONTENT_LENGTH'] > $max_size) {
        return false;
    }
    return true;
}

こんな感じですかねー
拡張子を判断するにはFileInfo拡張モジュールを使うこともできます
ファイル内の特定の位置からマジックバイトシーケンスを見つけることでファイルのMIMEタイプを推測しているみたいなので、確実な方法ではないようです。

ただしWindows環境のPHPではデフォルトで拡張モジュールが有効になっていないので、

php.ini
extension=php_fileinfo.dll #;extension=php_fileinfo.dll

のようにphp.iniを変更しApacheを再起動してください

補足ですが累乗の計算はpow()関数を使うことも**を使うこともできるみたいですー

45
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
55