【PHP, JavaScript】画像を並び替えてアップロードする【後編】

前回の記事の続きです。

https://qiita.com/kai_163/items/9c52327739007f9be4df

前回は、
* アップロードする画像を選択したら、その場でプレビュー
* ドラッグ＆ドロップで画像の位置を入れ替える
* 最終的な画像の位置情報と、画像を一緒に送信

までやりました。全てJavaScriptでの処理でしたが、今回はPHPのみの処理です。送信された位置情報を使って、アップロードされた画像をプレビューと同じ並びで表示します。

HTMLの構造が同じなので、前回作成したCSSファイル`photos_view.css`を読み込みます（前回の、「作成するファイル」のリンクからご覧ください）。
## 作成するファイル
* [upload_process.php【完成】](#upload_processphp完成)

## 今回やること
[（1）位置情報photoOrderをJSON形式からデコード＆画像の枚数を取得](#1位置情報photoorderをjson形式からデコード画像の枚数を取得)
[（2）エラーチェック](#2エラーチェック)
[（3）エラーが無ければ並び替えて画像を表示](#3エラーが無ければ並び替えて画像を表示)
```php:upload_process.php
<?php
//*********************************（1）*******************************
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
//*********************************************************************
?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="./css/photos_view.css"/>
  </head>
  <body>
    <div id="photos-view">
      <div id="all-photos" class="layout-<?=$count_files;?>">
        <?php
        //*******************************（2）***************************************
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
              $error_msg = 'コンテンツタイプが画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else{
            //*******************************（3）**********************************
              $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
              $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
              fclose($fp);
              $encode_photo = base64_encode($get_photo);
              $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
              print '<div><img src="data:' . $photo_info['mime'] .';base64,' . $encode_photo . '"></div>';
            }
          }
        ?>
      </div>
    </div>
  </body>
</html>
```
## アップロードされたファイルはどこにある？
* アップロードされたファイルの情報は`$_FILES['photo']`にある。
* `['photo']`とは、\<input type="file">に指定したname属性の値。
* 各項目ごとに「ファイル名の昇順」で格納される（ブラウザによって違う？）。

<img width="400" alt="スクリーンショット 2023-11-29 161938.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/f8a75be0-fa4c-3a94-ced5-181ee2e57125.png">

* `$_FILES['photo']['type']`
・アップロードしたファイルのコンテンツタイプ
* `$_FILES['photo']['tmp_name']`
・アップロードしたファイルは、「サーバ上の一時的なファイル」に「仮保存」される。その一時ファイル名。つまり、アップロードしたファイルを取得するにはこの一時ファイルにアクセスする。例えば、
1. `move_uploaded_file`関数を使って、一時ファイルから本来の保存先へファイルを移動。
保存先はドキュメントルートの外にする。不正なファイルがドキュメントルート下にアップロードされるとそのまま実行されてしまうから。
2. 一時ファイルを`fopen`関数で開いて、アップロードしたファイルの内容を`fread`関数で読み取る。**【今回はこれによって画像を表示します】。**
* 一時ファイルは、`move_uploaded_file`関数で処理されなかった場合、スクリプトの処理後に自動的に破棄される。
## （1）位置情報photoOrderをJSON形式からデコード＆画像の枚数を取得
```php:upload_process.php
<?php
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
?>
```

## （2）エラーチェック
画像を1枚ずつ、3つのエラーチェックを行います。
[①アップロードに関するエラーチェック](#アップロードに関するエラーチェック)
[②拡張子をチェック](#拡張子をチェック)
[③コンテンツタイプをチェック（②より信頼性あり）](#コンテンツタイプをチェックより信頼性あり)

### ①アップロードに関するエラーチェック
アップロードされたファイルの情報は`$_FILES['photo']`にあります。各項目ごとに「ファイル名の昇順」で格納されています（ブラウザによって違う？）。

<img width="400" alt="スクリーンショット 2023-11-29 161938.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/f8a75be0-fa4c-3a94-ced5-181ee2e57125.png">


つまり、for文を使って「画像を1枚ずつチェック」することができます。例えば`$_FILES['photo']['name'][$i]`とすると、ファイル名を1つずつ取り出します。「`$i`は各ファイル」です。

エラーチェックがしたいので、`$_FILES['photo']['error']`を見ます。
```
 [error] => Array ( [0] => 0 [1] => 0 [2] => 0 [3] => 0 ) 
```

`$_FILES['photo']['error']`には、0～8（5は無い）の数字が格納されます。PHPに元から定義されている定数の値と一致します。これを使って**アップロード時のエラーチェック**ができます。

|定数  |値  |概要  |
|---|---|---|
|UPLOAD_ERR_OK  |0  |アップロード成功  |
|UPLOAD_ERR_INI_SIZE  |1  |php.iniのupload_max_filesizeで指定サイズを超えた  |
|UPLOAD_ERR_FORM_SIZE  |2  |HTMLのmax_file_sizeで指定したサイズを超えた  |
|UPLOAD_ERR_PARTIAL  |3  |ファイルが途中で切れている  |
|UPLOAD_ERR_NO_FILE  |4  |ファイルが存在しない  |
|UPLOAD_ERR_NO_TMP_DIR  |6  |一時保存のためのフォルダーが存在しない  |
|UPLOAD_ERR_CANT_WRITE |7  |ディスクへの書き込みに失敗した  |
|UPLOAD_ERR_EXTENSION |8  |拡張モジュールによってアップロードが中断された  |

```php:upload_process.php
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }
         }
```
### ②拡張子をチェック
* 配列に、許可する拡張子のリストを用意する。`['gif', 'jpg', 'jpeg', 'png']`
* pathinfo関数は、ファイルパスに関する様々な情報を連想配列で取得する。その中にある拡張子`['extension']`を取りだす。取り出した拡張子をstrtolower関数で小文字に変換。
* 拡張子が配列のリストの中になければエラー。
```php:upload_process.php
  ...}else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
     }
```
### ③コンテンツタイプをチェック（②より信頼性あり）

* 配列に、許可するコンテンツタイプのリストを用意する。`['image/gif', 'image/jpg', 'image/jpeg', 'image/png']`
* finfo_file関数は、ファイルの内容を解析し、コンテンツタイプを取り出す。引数に「FileInfoインスタンス（finfo_open関数で生成）」と「ファイル名」を渡す。
* コンテンツタイプが配列のリストの中になければエラー。

```php:upload_process.php
else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
    $error_msg = 'コンテンツタイプが画像ではありません。';
    die('<div>'.$error_msg.'</div>');
}
```
* `FILEINFO_MIME_TYPE`はmimeタイプを返す。`finfo_open(FILEINFO_MIME_TYPE)`はコンテンツタイプを取得することを意味する。
* `$_FILES['photo']['tmp_name']`は、アップロードされたファイルが仮保存されている一時ファイル名。

:::note warn
`$_FILES['photo']['type']`でもアップロードしたファイルのコンテンツタイプを取得できる。しかし、悪意のあるユーザーにとって、typeキーの内容を偽装するのは簡単。予備的なものにとどめるべき。
:::

## （3）エラーが無ければ並び替えて画像を表示
プレビューと同じ並びで表示します。プレビュー時の画像の位置情報である、配列photoOrderの順番に表示処理をします。今まで`$i`を使って処理していましたが、`photoOrder[$i]`にします。

<img width="400" alt="スクリーンショット 2023-11-30 124315.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/a43de3ab-63ec-8917-0f84-541d120ddb9b.png">

```php:upload_process.php
}else{
 $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
 $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
 fclose($fp);
 
 $encode_photo = base64_encode($get_photo);
 $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
 print '<div><img src="data:' . $photo_info['mime'] . ';base64,' . $encode_photo . '"></div>';
}
```
2つに分けて説明します。
```php:upload_process.php
 $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
 $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
 fclose($fp);
```
* [fopen関数](https://www.php.net/manual/ja/function.fopen)・・・ノートの表紙を開くイメージ。
・ファイルやURLを開き、「ファイルポインタリソース（ファイルハンドル）」を返す。
・ファイルハンドルとは、「ファイルを操作するためのキーとなる情報」。ファイルに対する読み書きはファイルハンドルに対して行う。
・引数に「開きたいファイル名」と「開くモード」を指定。
・このコードでは、`$_FILES['photo']['tmp_name']`（アップロードされたファイルが仮保存されている一時ファイル）を、`rb`モードで開く。
* `rb`モード
`r`は読み込み専用。他にも種類あり。モードの後ろに`t`（テキストモード）か`b`（バイナリモード）を指定する。省略すると`b`になる。`t`はファイル内の改行を、OSに合ったものに変換する。`b`は何もしない。`t`に頼らず正しい改行コードを書き、`b`を使うのが強く推奨。
* [fread関数](https://www.php.net/manual/ja/function.fread)
・「ファイルポインタリソース（ファイルハンドル）」から最高○バイトを読み込み、読み込んだ文字列を返す。引数に「ファイルポインタリソース」と「最大バイト」を指定。最大バイトは、`filesize`関数で取得したファイルサイズ。つまり、ファイル全てを読み込む。
* `fclose`関数・・・開いていたノートを閉じるイメージ
・オープンしたファイルポインタをクローズする

```php:upload_process.php
$get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
//........省略
$encode_photo = base64_encode($get_photo);
$photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
print '<div><img src="data:' . $photo_info['mime'] . ';base64,' . $encode_photo . '"></div>';
```
* `base64_encode`関数・・・指定した文字列をbase64でエンコードして返す。
* [getimagesize関数](https://www.php.net/manual/ja/function.getimagesize)・・・画像の大きさを取得
・引数に「情報を取得したいファイル（名）」を指定。ここでは「データURL」で指定してる。
```
getimagesize関数の結果（例）
Array ( [0] => 1200                           ...幅
        [1] => 800                            ...高さ
        [2] => 2                              ...画像の形式。PHPに定義されている定数の値
        [3] => width="1200" height="800",     ...<img>で使える
        [bits] => 8                           ...色のbit
        [channels] => 3                       ...RGB画像は3、CMYK画像は4 
        [mime] => image/jpeg )                ...（★）画像のMIMEタイプ
```
・\<img>のsrc属性の値も「データURL」。データURLを表現するのに（★）画像のMIMEタイプを使う。
* [データURL](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)（`'data:application/octet-stream;base64, . $encode_photo'`）
・「data:」がついたURL。小さなファイルをインラインで文書に埋め込むことができる。「data:**MIMEタイプ;base64,データ本体**」で表す。
* [**MIMEタイプ**](https://e-words.jp/w/application-octet-stream.html)
「タイプ/サブタイプ」。`text/plain`、`text/html`など。
タイプ
・`text`・・・文字データ
・`image`・・・画像
・`video`・・・動画
・`audio`・・・音声
・`application`・・・上記以外。（例）圧縮ファイルは「application/zip」
サブタイプ
・`application`のサブタイプで、「IANAメディアタイプ」に登録されていない形式のものは`octet-stream`とする。IANAメディアタイプとは、MIMEタイプの標準規格。
・つまり`application/octet-stream`は、任意のバイナリファイルや、ファイル形式が不明な時に使う。
* **;base64**
・テキストデータ以外の時に書く。続く「,」下の**データ本体**には、base64エンコードしたバイナリデータを指定する。

## upload_process.php【完成】
```php:upload_process.php
<?php
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="./css/photos_view.css"/>
  </head>
  <body>
    <div id="photos-view">
      <div id="all-photos" class="layout-<?=$count_files;?>">
        <?php
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
              $error_msg = 'コンテンツタイプが画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else{
              $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
              $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
              fclose($fp);
              $encode_photo = base64_encode($get_photo);
              $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
              print '<div><img src="data:' . $photo_info['mime'] .';base64,' . $encode_photo . '"></div>';
            }
          }
        ?>
      </div>
    </div>
  </body>
</html>
```

【参考】
・『独習PHP 第4版』　山田祥寛著
・[PHPでの画像の保存・表示方法まとめ](https://qiita.com/okdyy75/items/669dd51b432ee2c1dfbc "PHPでの画像の保存・表示方法まとめ")