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/css
やtext/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
の中にファイル自体が配列として入っており 更にそのファイルそのものの名前やファイルサイズファイル形式諸々もろが格納された連想配列になっているという寸法
イメージ図
なので実際にそれを扱う場合には例えば$_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
:ファイル名type
:MIMEタイプ 上で説明した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.ini
のupload_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、これは逆に発生が想定されていないもの、「実運用で起こられると困る」ものに使う、これは明確にバグ