おべんきょうメモ

Web制作の勉強メモ ほぼほぼ自分用

PHPメモ 画像のアップロード

前提:form/inputタグの設定

ファイル送信にはformタグ及びinputタグを使いますが それ周りでいくつか注意すべき点があるので書いときます

inputタグでファイルを送信したい場合にenctype="multipart/form-data"の記述が絶対必要という話

POST送信なのでいつものごとくformタグにmethod="post"の記述は必要なんですが(未だによく忘れる……)画像のアップロードをするために必要不可欠な記述が

<form enctype="multipart/form-data">

まずenctypeってなんやねんって話ですがこれはMIMEタイプ、まぁわかりやすく雑に言ってしまうと扱うファイルタイプを指定する項目になります、これ以外にもtext/plainとかimage/pngとか、あとhtmlコーディングしてると見る機会がよくあるtext/csstext/javascriptもそうです
んで、multipart/form-dataはmultipartと名のある通り複数の種類のデータを一度に送信できる「複合データ型」というものになります(正確にはmultipartと名が付くタイプにはmultipart/byterangesというのもあるのですが今回はformからの送信の話なのでform-dataを使うんだと思います)

で、なんでこれの記述をする必要があるのかというと、詳しくはURLを参照してほしいのですが(えぇ……)デフォルトだとファイルを読み込んでもサーバーに送信されるのはファイルがあるという事実とファイル名のみだけになってしまい、実際のファイルの中身まではサーバーに送ってくれないのです!
なのでサーバー側は実際のファイルを扱うことが出来ず、以後の処理が出来なくなってしまいます、気をつけましょう
僕はこれの記述忘れに気が付かず1週間くらい消費しました

参考
mugenup-tech.hatenadiary.com
www.yoheim.net

input type="hidden"とアップロードファイルサイズの話

input typeにいろいろな種類があることは知っていますが、最近になって初めて知ったtype="hidden"、まぁ字面の通り画面に直接描画はせず隠しデータのようなものを一緒に送信することが出来るものです(あくまで画面に表示されないってだけの話なのでソースを見れば普通に見えます)
これを利用してアップロード出来るサイズ上限を指定することが出来ます、必須ではないのですが覚えておくと便利なことがあるかもしれません

具体的なやり方としてはtype:hiddenのinputタグにname:MAX_FILE_SIZEを設定し、valueに実際の値を入れればOKです

<input type="hidden" name="MAX_FILE_SIZE" value="3145728">

3145728 は3MBの意です 1024×1024×3=3145728

ただファイルサイズはこことは別にもう1つ、php.iniでも設定が必要です
ここの制限はクリアしててもphp.ini 側で引っかかって上げられない、という場合も普通にあるので予め設定しておきましょう

php.ini のファイル上限関係のパラメーター

  • upload_max_filesize:アップロード出来る最大サイズ
  • post_max_size:POST送信で扱えるファイルの最大サイズ
  • memory_limit:サーバーのメモリの最大サイズ

ファイル関係のスーパーグローバル変数$_FILES

送信されたファイル情報については連想配列形式のスーパーグローバル変数 $_FILESに格納されます
具体的にどういう状態で格納されているかというと まず$_FILESの中にファイル自体が配列として入っており 更にそのファイルそのものの名前やファイルサイズファイル形式諸々もろが格納された連想配列になっているという寸法

イメージ図

f:id:mi28xider:20181206101335p:plain

いつもの

なので実際にそれを扱う場合には例えば$_FILESの中にimage1というファイルがあったとしてそれの実際のファイル名を取得したいんや!というとき、本来なら$_FILES['image1']['name']という書き方をすることになります

が、キーが2つも連続すると見栄え的に混乱する人もいると思う(ワイやぞ)ので画像そのものを任意の変数に格納して、それの情報がほしいときにキーを添えた書き方をすると混乱しにくいかもという提言(ひとり会議)

$_FILES['img01']['name'];
$_FILES['img02']['name']; 
$_FILES['img03']['name'];  

と本来はなるところを

$img01 = $_FILES['img01'];  
$img02 = $_FILES['img02']; 
$img03 = $_FILES['img03'];

$img01['name']
$img02['name']
$img03['name']

としてやるとわかりやすいんちゃう?という話

$_FILESに格納されるファイル情報

  • name:ファイル名
  • typeMIMEタイプ 上で説明したimage/pngとか
  • size:ファイルサイズ
  • tmp_name:ファイルをアップロードする際、ファイルは実は一度一時ファイルとして保存されているが、そのときの名前
  • error:読んで字の如くエラー エラーに応じた数字が格納 ちなみにエラーがない場合も0として表現される

実際にアップロードさせる流れ

前提

inputタグはこう記述されているのを前提

<input type="file" name="image">

で、上の話に則って

<?php
$file = $_FILES['image'];

として画像を変数に代入しておく

ファイルの有無をチェック

まず何よりファイルが送られていなければお話にならないので$_FILES関数の中にファイルが有るかをチェック
ファイル情報どれかをチェックすれば有無は確認できると思うけどあとでバリデーションに使う関係もあるのでerrorでチェックしてしまおう
上で言ってるけど特にエラーが無くてもerrorの中には0が格納されます

<?php
if (isset($file['error']) && is_int($file['error'])) {

isset()は「変数が設定されていること」「変数がnullでないこと」をチェックしてくれる関数
データが入っていればここは必ずtrueになるはず!
で、is_int()は整数型かどうかを見てくれる関数、errorは整数で見るのでここがちゃんと整数か念の為確認する

バリデーションチェック

画像にもバリデーションチェックがある
上で言ったようにerrorは整数で返されるが、アップロードの際にPHPはその数字を格納した便利な定数を準備してくれる、出来る子や
いくつかのエラーは例外を投げてやり、try-catchで処理を振り分ける
(現時点で)覚えとくといいエラーコード

  • UPLOAD_ERR_OK=0:エラーなし
  • UPLOAD_ERR_INI_SIZE =1:php.iniupload_max_filesizeに設定した値をファイルサイズが超過している
  • UPLOAD_ERR_FORM_SIZE=2:inputフォームで設定されたファイルサイズを超過している
  • UPLOAD_ERR_NO_FILE=4:ファイルがアップされていない

これらの定数と$file['error']の値を比較する、選択肢が多いからswitch文がいいかもね

<?php
try {
  switch ($file['error']) {
    case UPLOAD_ERR_OK:
      break;
    case UPLOAD_ERR_NO_FILE:
      throw new RuntimeException('ファイルが選択されていません');
    // 中略
    default:
      throw new RuntimeException('その他のエラーが発生しました');
  }

エラーが起きたら例外処理に投げてあげる
詳しいことはググろう 過去にもすこし言及したね
328xider.hateblo.jp

ファイル形式のチェック

ファイル形式自体は$_FILES['type']にも格納されているが、ここのMIMEタイプは割と簡単に偽装できちゃうらしい
ということでファイルのexif情報を見て独自に判別してやる

<?php
$type = @exif_imagetype($file['tmp_name']);
  if (!in_array($type, [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF], true)) {
    throw new RuntimeException('画像形式が未対応です');
  }

画像のexif情報の中にある画像形式をexif_imagetype()という関数で取り出せる
@を関数の前につけるとその関数で発生したエラーメッセージは無視されるらしい
in_array()は配列の中に一致するものがあるかを調べる関数

<?php
in_array(比較するもの, 配列[], 型比較をするかどうか);

in_arrayには第三引数がある。型の比較をするとかしないとか表現がわかりにくいのだけれど、実際はfalseのときは == で、trueのときは === で比較されると覚えておけばいい。

とのこと

exif_imagetype()で取り出したファイル形式がIMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIFのうちのどれかに一致しているかを調べている 一致して無ければ例外を投げちゃう

一時ファイルを正式な保存箇所に移動してやる=アップロード

上でチラッと言ったけどファイルをinputで送信した時点で実はそのファイルは一時ファイルとして保存がされていたりする その一時ファイルを僕らが把握できる箇所に移動してやる、ということがアップロードの本質なのである!(たぶん)
まずはファイルの保存箇所を変数に格納

<?php
$path = './img/upload/' . sha1_file($file['tmp_name']) . time() . image_type_to_extension($type);

sha1_file()はファイル名をハッシュ化=暗号化する関数 ファイル名の重複防止
でも自分で試してたときに「全く同じファイル」だと重複して上書きされちゃうことに気がついた だから念の為UNIXタイムスタンプを付けてより重複しないようにしてみたのがこれ
image_type_to_extension()exif情報のファイル形式から拡張子を取得してくれる(という認識でいいはず……)

で、ファイルを実際の保存場所に移動しちゃう
ついでにダメだった場合にエラーも投げちゃう

<?php
if (!move_uploaded_file($file['tmp_name'], $path)) {
  throw new RuntimeException('ファイル保存時にエラーが発生しました');
}

move_uploaded_file()がファイルを(実際のディレクトリに)移動する関数

<?php
move_uploaded_file(元の場所('概ね$_FILES['tmp_name']かな'), 移動先);

move_uploaded_file()はもし関数が正常に実行できた場合に返り値としてtrueを返してくれる
なのでこれを利用して「trueが返ってこなかった場合」にエラーを投げてるんだね

最後にファイルのパーミッションを変更

<?php
chmod($path, 0644);

ファイルのパーミッションを変えてやる
詳細は割愛しますが644は「自分は読み書き可、他人は読み取りだけ可」の意味です

これでアップロードは完了
$pathにURLが格納されているのでアップロードしたファイルを他(DBとか)で使う場合はこの$pathの値を利用すればいいんですね 関数化してるならreturnでこれを返すのもよい

投げられた例外

catch部分の話 今回はだいたい手動(throw new~)で例外を投げているので、投げたときに添えたメッセージが$e ->getMessage()で取り出せる
ユーザーに表示させる部分にもこれが利用できるので楽ですね

RuntimeExceptionってなんぞや

今回はただのExceptionではなくRuntimeExceptionを利用している
これは運用で発生することが発生しうる例外、「使い方によっては普通に起こりうる」ことに使うことが多い、厳密にはバグではないもの
ちなみにこれの対にあるのがLogicException、これは逆に発生が想定されていないもの、「実運用で起こられると困る」ものに使う、これは明確にバグ