Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5応用編④】アップロード関係

A子

トップページのデザインを試行錯誤で作ってみたよ

あくまでも試案と言うか、叩き台だけどね
(ここからさらにブラッシュアップしていけば良いんじゃないかな)

B美

かなり苦労したみたいね

A子

まぁね

C菜と二人で「ああだ、こうだ」と結構時間をかけたよ

C菜

大変でした~

でも、良い経験になったと思います~

B美

では見せてもらおうか、連邦のモビルスーツの性能とやらを

A子

ふるっ!
あんた私と同い年よね?

ま、まぁ良いわ
これがトップページの「index.php」よ!


C菜

で、これがブラウザ画面です~


A子

ちなみに、「ページネーション」の結果がよく分かるように、1ページの件数を2件にしているわ
(CakePHP5基礎編⑤で習ったからね)

C菜

画像ファイルのアップロード処理を書いてないので、画像の埋め込みまでは行えませんでしたけどね~

B美

予想以上の出来栄えに、ちょっとビックリ(笑)

あ、ついでに「PostsController.php」の「index」メソッドを変更しておきましょう

$this->Posts->find()->where(['delete_flag' => 0]);
$posts = $this->paginate($query, ['limit' => MAX_PAGE, 'order' => ['id' => 'desc']]);

C菜

「delete_flag」が「0」のレコードのみ抽出で、「ページネーション」時に並べ替えを行うって意味でしょうか~?

つまり、削除レコードの「delete_flag」には「0」以外(例えば「1」)をセットするわけですね~

B美

その通り!

あと、並べ替えについては、「id」の降順でソートする…ってわけ

A子

MAX_PAGE」って定数よね?
つまり「bbsapp/config」ディレクトリ内の「const.php」にこの定義を追加すれば良い…ってことか

define("MAX_PAGE", 2);

B美

よく分かってるじゃん!

定数化しておくと、あとになって変更するときにめっちゃ便利なのよね
あと、投稿番号の「背景色」とタイトルの「背景色」の両方についても定数化しておくと良いかもよ
(まぁ、別にやらなくても良いんだけど)

C菜

ついでなので、やってみるです~

define("NUM_COLOR", "#b0e0e6");    //投稿番号の背景色
define("TITLE_COLOR", "#fff8dc");  //タイトルの背景色

を「const.php」に追記して…っと~

A子

えっと、まずは「PostsController.php」を変更しよう

C菜

さらに「bbsapp/templates/Posts」ディレクトリ内の「index.php」も書き換えます~

A子

ブラウザで実行確認してみると…


C菜

バッチリです~

A子

それじゃ、次は「新規投稿」画面のレイアウトなんだけど、こんな感じで作ってみたよ

あ、上から4行目の背景色指定の箇所については、さっきの定数で書き換えておいたから…


C菜

上のコードをブラウザで見たのが、下の画面になります~

B美

ふむ、素晴らしい!

予想以上の出来栄えと言っても過言ではないわね

A子

あー、たださぁ
Controllerコントローラーである「PostsController.php」なんだけど…

C菜

「view」メソッドと「edit」メソッドは全て削除して、「add」メソッドと「delete」メソッドは下記の状態にしました~

でも、ここから先が分かりません~

B美

「PostsController.php」をコーディングする前に、画像ファイルの取扱いについての詳細仕様を固めないといけないの

ファイルとして保存するのだから、まずはその格納場所を決めましょう

C菜

一般的にはどこに格納するんでしょうか~?

B美

「bbsapp/webroot」の下ね

A子

何でよ

B美

そこが、このWebアプリケーションの「ドキュメントルート」だから…

C菜

つまりはインターネット上に公開される場所…ってことですか~?

あ、だから「bbsapp/webroot」の中にCSSファイルを格納している「css」ディレクトリや画像ファイルを格納している「img」ディレクトリがあるんですね~

A子

だったら、アップロードした画像ファイルは「bbsapp/webroot/img」に入れれば良いんじゃないの?

B美

もちろんそれでも良いんだけど、今回は別のディレクトリを新規作成してみましょう
(学習の一環として…)

MATE端末」を開いて、次のように入力してね

cd html/bbsapp/webroot[Enter]
mkdir files[Enter]
chmod 757 files[Enter]

これで「bbsapp/webroot」の下に「files」ディレクトリが生成されるわ

A子

「chmod 757 files」って何なのよ?

B美

ディレクトリのパーミッションを設定しただけよ
要するに、「アクセス権限」の設定ね

まぁ、あまり気にしなくても良いわ
(「webroot」の下にディレクトリを作るときに実行する「おまじない」みたいなものだと思っていればOK)

注:パーミッションについては、人によって異なる考え方があります
(ゆるくするか、厳しくするか…の違い)

A子

んで、そこにはオリジナルのファイル名で格納するわけじゃないのよね?
(データベース設計のときにそう言ってたし…)

B美

ええ
オリジナルのファイル名が日本語だったり、格納するファイル名が重複したりするとまずいからね

さて、どうする?

C菜

ランダムなファイル名をプログラムで作ったらどうでしょうか~?

半角で20文字以上とかだったら、ファイル名が重複することはないと思うんですけど~

A子

うーん、投稿番号って1から始まる連番なんだから、その番号に拡張子を付けるだけで良いんじゃない?
(アップロードできるファイル数は、0個または1個なんだし…)

B美

どちらの方法でもうまくいくと思うわよ

ただし、C菜の方法では「重複しないことを担保する仕組み」が必要だし、A子の方法の場合「投稿番号をどのように取得するか」がポイントになるけどね

C菜

そういえば「投稿番号」はどうやって決まってるんですか~?

B美

データベースのテーブル設計の際、主キーである「id」に「auto_increment」って付けたのを憶えてる?

その属性が付いている場合、データベース側で勝手に連番を付けてくれるのよ
(それが「投稿番号」ってわけ)

A子

んん?
だとすると、レコードを新規作成しないと「id」の値は決まらないのか…

あれ?
「filepath」に入れる文字列って、「id」の値が決まらないと付けられないわけだから、「ファイル名を投稿番号にする」案ってダメじゃん…

B美

ダメってことは無いわよ

まずは(アップロードするファイルが有ろうが無かろうが)「filename」と「filepath」フィールドに空文字列をセットしたレコードを新規作成します
(もちろん「タイトル」や「本文」、「削除フラグ」には値をセットしてね)

で、もしもファイルが有る場合には…
・「id」の値を取得し、その値に拡張子を付けたものをファイル名とし、ファイル保存
・次に、ファイル保存したパスを「filepath」フィールドに設定してレコードを更新
(もちろん「filename」フィールドにはオリジナルのファイル名をセットすること)

C菜

なるほどですね~

そっちのほうが簡単そうですし、A子社長の案でいきましょう~

B美

分かったわ
それじゃ、まずは全体の流れを説明するわね

1.もしもPOSTされたら「$this->request->getData('○○')」で送信されてきたデータを取得します

例えば
if ($this->request->is('post')) {
  $○○ = $this->request->getData('○○');
  ・・・
}

A子

そこまでは、もうできてるわよ

B美

2.「$this->Posts->newEmptyEntity()」メソッドの戻り値を変数へ代入し、その変数を使って値をセットしていきます

例えば
$post = $this->Posts->newEmptyEntity();
$post->○○ = △△;

…って感じね

C菜

えっと~

$post = $this->Posts->newEmptyEntity();
$post->title = $title;
$post->body = $body;
$post->filename = '';
$post->filepath = '';
$post->delete_flag = 0;

…ということでしょうか~?

B美

おぉ、良いじゃん良いじゃん

ただし、まだデータベースへの登録はできていないからね
そのためには…

3.「$this->Posts->save()」メソッドを呼び出して、戻り値が「true」だったら成功、「false」ならば失敗よ
(このメソッドへの引数は、さっきの「newEmptyEntity()」の戻り値を格納した変数を渡します)

A子

ああ
…ってことは

$this->Posts->save($post);

で良いのかな?

C菜

成功か失敗かを判別したほうが良いんじゃないでしょうか~?

if ($this->Posts->save($post) == true) {

} else {

}

…って感じで~

A子

「== true」は要らないんじゃないの?

C菜

そうでしたぁ~

if ($this->Posts->save($post)) {

} else {

}

これでどうでしょう~?

B美

うん、良い感じ

4.画像ファイルが指定されたかどうかを判別するには、「$upload->getClientFilename()」メソッドの戻り値が空文字列かどうかで分かるの
(これによってオリジナルのファイル名を取得できるわ)

C菜

だったらこうです~

$original_filename = $upload->getClientFilename();
if ($original_filename != '') {
  (ファイルがあるときの処理)
}

B美

それじゃ、次ね

5.さっきデータベース登録(レコード挿入)したことで主キーの値が決まったから、その値を知るには「$post->id」でOKよ

A子

だったら、ここでこうすれば良いんじゃない?

if ($original_filename != '') {
  $new_id = $post->id;
}

B美

そういうことね
んじゃ、次!

6.ファイル名を決めるには「拡張子」が必要だから、それを取得しましょう
「pathinfo($original_filename, PATHINFO_EXTENSION)」関数を呼べば、その戻り値が「gif」や「png」って感じになるわよ

あ、ただし、人によっては「大文字」で拡張子を付けてるかもしれないから注意してね

C菜

拡張子が正しいかどうかのチェックをするためにも、大文字か小文字かは統一しておきたいですよね~

B美

それは「mb_strtolower()」関数(小文字へ変換)や「mb_strtoupper()」関数(大文字へ変換)で実現できます

一般的には「小文字」に統一する人のほうが多いかな

A子

それじゃ、小文字に変換しよう

if ($original_filename != '') {
  $new_id = $post->id;
  $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
  $filepath = $new_id.".".$extension;
}

…で、どうよ

B美

ちょっと惜しい
このWebアプリのドキュメントルートの下の「files」ディレクトリ内に格納したい場合、「$filepath」はこうなるの

$filepath = ".".DS."files".DS.$new_id.".".$extension;

ちなみに、先頭の「.」はカレントディレクトリって意味なんだけど、ここでは「ドキュメントルート」って意味になるわ

C菜

DS」って何ですか~?

B美

ディレクトリとディレクトリまたはディレクトリとファイルを区切るための「区切り文字」よ

定数になっている理由は、実行環境(Webサーバ)がLinuxでもWindowsでもうまく動くように…
(Linuxでは「/」だけど、Windowsでは「\」が区切り文字だからね)

さぁ、いよいよファイルの保存を行います

7.「$upload->moveTo()」メソッドを呼べば、指定した場所に指定した名前で保存されるわ
(このメソッドの引数には、ファイルの「フルパス」を渡すってこと)

A子

だったら、こうなるわね

if ($original_filename != '') {
  $new_id = $post->id;
  $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
  $filepath = ".".DS."files".DS.$new_id.".".$extension;
  $upload->moveTo($filepath);
}

そんなに難しくないかな

B美

そうしたら、次はさっきのレコードを更新しないとね
($original_filenameを「filename」フィールドに、$filepathを「filepath」フィールドにセットしてから更新しないとね)

レコード更新の手順も割と簡単よ

8.「$this->Posts->get(主キー番号)」の戻り値を変数へ代入し、その変数を使って値をセットしていくだけ

C菜

こんな感じですかね~

$new_post = $this->Posts->get($new_id);
$new_post->filename = $original_filename;
$new_post->filepath = $filepath;

もしかして、このあとsaveメソッドを呼ぶんですか~?

B美

まさにその通り!

どうするか分かる?

C菜

$new_post = $this->Posts->get($new_id);
$new_post->filename = $original_filename;
$new_post->filepath = $filepath;
if ($this->Posts->save($new_post)) {

} else {

}

…って感じでしょうか~?

B美

そういうこと!

あ、でも失敗したときだけ何らかの処理をすれば良いから

if ($this->Posts->save($new_post) == false) {

}

または

if (!$this->Posts->save($new_post)) {

}

…って感じかな

A子

下の例の左端にある「!」って何だっけ?

B美

忘れたの?

論理演算子の「not」を意味する記号よ
(要するに、真偽を逆転させるってこと)

C菜

失敗時にはどうするんですか~?

B美

フラッシュメッセージを出してから、リダイレクトするのが一般的ね
例えば「$this->Flash->success('成功時のメッセージ')」は成功時のメッセージで、「$this->Flash->error('失敗時のメッセージ')」が失敗時のメッセージよ

あと、リダイレクトというのは、別のページへ移動すること
例えば「return $this->redirect(['action' => 'index']);」を実行した瞬間、トップページである「index」に遷移するってわけ

A子

だったらこうなるかな?

if (!$this->Posts->save($new_post)) {
    $this->Flash->error('画像ファイル情報のデータベース登録に失敗しました。');
    return $this->redirect(['action' => 'index']);
}

B美

OK、OK

C菜

ちょっとまとめてみましょうか~

if ($this->request->is('post')) {
    $title = $this->request->getData('title');
    $body = $this->request->getData('body');
    $upload = $this->request->getData('upload');

    $post = $this->Posts->newEmptyEntity();
    $post->title = $title;
    $post->body = $body;
    $post->filename = '';
    $post->filepath = '';
    $post->delete_flag = 0;

    if ($this->Posts->save($post)) {
        $original_filename = $upload->getClientFilename();

        if ($original_filename != '') {
            $new_id = $post->id;

            $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
            $filepath = ".".DS."files".DS.$new_id.".".$extension;
            $upload->moveTo($filepath);

            $new_post = $this->Posts->get($new_id);
            $new_post->filename = $original_filename;
            $new_post->filepath = $filepath;
            if (!$this->Posts->save($new_post)) {
                $this->Flash->error('画像ファイル情報のデータベース登録に失敗しました。');
                return $this->redirect(['action' => 'index']);
            }
        }
    } else {
        $this->Flash->error('新規投稿のデータベース登録に失敗しました。');
        return $this->redirect(['action' => 'index']);
    }

    $this->Flash->success('新たな投稿を登録しました。');
    return $this->redirect(['action' => 'index']);
}

B美

さすがはC菜ね
よくまとまっているわ

それじゃ、実際に上記のコードを打ち込んでから、実行確認してみてね

A子

打ち込んだよ

ちなみに、コメントも入れてみた


C菜

WindowsのEdgeからアクセスしてみたです~

A子

この画面に画像は表示されていないけど、ちゃんとアップロードはされたみたいだね

なんだかあっさりと成功したよ
(B美先生のおかげなんだけど…(苦笑))

B美

それじゃ、あとは「bbsapp/templates/Posts」の中の「index.php」を修正して画像表示までやっちゃって!

C菜

こんな感じで、赤枠の箇所を追加してみました~

A子

んで、ブラウザで確認してみると…

B美

ふむ
なかなか良いんじゃないかしら

さて、それじゃ今回はここまでにしておきましょう
次回は、アクセスログ記録用テーブルに対する操作をやっていきます
(とは言っても、全然難しくはないからね)

A子

ほんとかよ(苦笑)