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

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``文で判別しているだけです

```php
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();
}
```
これは以下のようにも書き換えられます

```php
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()``関数を使うことも``**``を使うこともできるみたいですー