第二回に引き続き、掲示板の作成を行っていきます。
今回は
- 画像投稿機能の実装
を行っていきます。
#目次
~第一回 CRUDの実装~
~第二回 paginate, ナビゲーションバーの実装~
~第三回 画像投稿機能の実装~
~第四回 ドラッグアンドドロップ(DnD)での画像添付 ~
#方針
- inputフォームから画像をアップロードする
- 形式は ipg, png, gifのみ(最大サイズ 10M)
- DBには以下の項目を格納する
- img_name (ユニークな名前)
- img_ext (ファイルの拡張子)
- img_size (ファイルの容量)
- ファイルはユニークな名前をつけてローカル(webroot/img以下)に保存する
- その際、サムネイルも生成する
- webroot/img/mini以下に保存
- 表示する際はサムネイルで表示し、画像がクリックされた際にlightbox形式で表示する
#アップロードフォームの実装
前回作成したformエレメントを少し編集します。
php.src/Template/Element/form.ctp
<?= $this->Form->create($post, array(
'url' => array('action' => $action),
'type' => 'file', //add
)) ?>
<fieldset>
<?php
echo $this->Form->hidden('postId');
echo $this->Form->hidden('resId');
echo $this->Form->input('title');
echo $this->Form->input('name');
echo $this->Form->input('content');
echo $this->Form->file('img'); //add
echo "<br>";
?>
</fieldset>
<?= $this->Form->button(__('投稿する')) ?>
<?= $this->Form->end() ?>
以上でアップロードフォームが生成されます。
#画像の保存
ブサイクなやり方ですが、ご容赦下さい。
- モデルの修正
- jpg, png, gif以外の拡張子の場合、バリデーションエラーを返すように修正
- ファイルサイズが10M以上の場合、バリデーションエラーを返すように修正
php.src/Model/Table/PostsTable.php
~~
$validator
->allowEmpty('img_ext')
->add('img_ext', ['list' => [
'rule' => ['inList', ['jpg', 'png', 'gif']],
'message' => 'jpg, png, gif のみアップロード可能です.',
]]);
$validator
->integer('img_size')
->allowEmpty('img_size')
->add('img_size', 'comparison', [
'rule' => ['comparison', '<', 10485760],
'message' => 'ファイルサイズが超過しています(MaxSize:10M)',
]);
~~
- コンポーネントの作成
- add, edit アクションの両方で用いるので、コンポーネント化し、使いまわせるようにします
- コントローラーからsaveメソッドを呼び出すことで以下のことを行います
- $postに img_name, img_size, img_extを設定
- md5(uniqid(rand(), 1))により、ユニークな文字列を生成し、nameに設定しています
- 画像をローカルに保存
- $postに img_name, img_size, img_extを設定
- ※サムネイル生成のところがややこしすぎるので、より良い方法を思案中
php.src/Controller/Component/ImgProcessComponent.php
<?php
namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;
/**
* ImgProcess component
*/
class ImgProcessComponent extends Component
{
function initialize(array $config) {
$this->controller = $this->_registry->getController();
}
//validation適用のため、rquestに img_name, img_ext, img_sizeを詰める
function save($request) {
$img = $request->data['img'];
$ext = pathinfo($img['name'], PATHINFO_EXTENSION);
$name = md5(uniqid(rand(), 1)).'.'.$ext;
$request->data['img_ext'] = $ext;
$request->data['img_size'] = $img['size'];
$request->data['img_name'] = $name;
}
//オリジナルとサムネイル作成
function generate($tmp_name, $post) {
move_uploaded_file($tmp_name, 'img/'.$post->img_name);
$original_file = 'img/'.$post->img_name;;
list($original_width, $original_height) = getimagesize($original_file);
$thumb_width = 300;
$thumb_height = round( $original_height * $thumb_width / $original_width );
if($post->img_ext === 'jpg') $original_image = imagecreatefromjpeg($original_file);
if($post->img_ext === 'png') $original_image = imagecreatefrompng($original_file);
if($post->img_ext === 'gif') $original_image = imagecreatefromgif($original_file);
$thumb_image = imagecreatetruecolor($thumb_width, $thumb_height);
imagecopyresized($thumb_image, $original_image, 0, 0, 0, 0,
$thumb_width, $thumb_height,
$original_width, $original_height);
if($post->img_ext === 'jpg') imagejpeg($thumb_image, 'img/mini/'.$post->img_name);
if($post->img_ext === 'png') imagepng($thumb_image, 'img/mini/'.$post->img_name);
if($post->img_ext === 'gif') imagegif($thumb_image, 'img/mini/'.$post->img_name);
}
}
- controllerの修正
php.src/Controller/PostsController.php
~~
public $components = array(
'ImgProcess' => array(),
);
~~
public function add()
{
$post = $this->Posts->newEntity();
if ($this->request->is('post')) {
//追加(validation適用のため、requestに色々詰める処理)--------------
if(!empty($this->request->data['img']['name'])) {
$this->ImgProcess->save($this->request);
}
//------------------------------------------------------------
$post = $this->Posts->patchEntity($post, $this->request->data);
if ($post->postId == -1) {
$post->postId = $this->Posts->find()
->order(['postId' => 'desc'])
->select(['postId'])
->first()['postId'] + 1;
$post->resId = 0;
}
if ($post->name === '') $post->name = '名無しさん';
if ($this->Posts->save($post)) {
//追加(ローカルに保存&サムネイル生成)-------------------------
if(!empty($this->request->data['img']['name'])) {
$this->ImgProcess->generate(
$this->request->data['img']['tmp_name'], $post);
}
//-------------------------------------------------------
$this->Flash->success(__('投稿されました.'));
if ($post->resId ===0) return $this->redirect(['action' => 'index']);
else return $this->redirect($this->referer());
} else {
//validation errorの表示準備--------------------------------------------
if($post->errors()['img_ext'])
$this->Flash->error(__($post->errors()['img_ext']['list']));
if($post->errors()['img_size'])
$this->Flash->error(__($post->errors()['img_size']['comparison']));
//--------------------------------------------------------------------
$this->Flash->error(__('投稿できませんでした. 再度お試し下さい.'));
return $this->redirect($this->referer());
}
}
$this->set(compact('post'));
$this->set('_serialize', ['post']);
}
~~
}
以上で、アップロードされたファイルをローカルに保存することができました。
#画像の表示
-
lightboxの導入
- 英語ですが、コマンドを追っていけば組み込むことができました
-
templeteの編集
- one_articleエレメントを編集します
- $this->request->webroot でwebrootのディレクトリを参照するURLを生成できます
- lightboxを使うため、タグ内を
<a href="<?= $this->request->webroot ?>img/<?= $post->img_name ?>" data-lightbox='image-1'>
のようにしています
- one_articleエレメントを編集します
php.src/Template/Element/one_article.ctp
~~
<div class='panel-boby'>
<div style="padding:10px">
<?= h($post->content) ?>
</div>
<div style="padding:10px">
<?php if (!empty($post->img_name)): ?>
<a href="<?= $this->request->webroot ?>img/<?= $post->img_name ?>" data-lightbox='image-1'>
<img src="<?= $this->request->webroot ?>img/mini/<?= $post->img_name ?>"
alt="<?= $post->img_name ?>" height="200" width="200"/>
</a>
<?php endif; ?>
</div>
</div>
~~
以上により、lightbox形式でローカルにある画像を表示することができました。
#削除機能
投稿が削除された際に、画像も一緒に削除するようにdeleteアクションを編集します。
例によってブサイクなコードですが、以下解説です。
- 削除対象が返信投稿なのか大元の投稿なのかを判別します
- resID=0の場合、すべての返信投稿を削除するためにpostIdが一致する投稿の画像の名前を取得し、配列に保存しておきます
- foreachでまわしていき、オリジナルとサムネイルファイルを削除していきます
- resId が0 でない場合、
$del_post->img_name
と一致するファイルを削除します
php.src/Controller/PostsController.php
~~
//画像削除のため
use Cake\Filesystem\Folder;
use Cake\Filesystem\File;
~~
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$del_post = $this->Posts->get($id);
if($del_post->resId === 0) { //1
//ファイル削除用に削除するファイル名を詰めた del_imgs を作成 //2
$query = $this->Posts->find()
->where(['postId =' => $del_post->postId])
->select(['img_name'])
;
$del_imgs = [];
foreach($query as $q) array_push($del_imgs, $q->img_name);
//-----------------------------------------------------------
if ($this->Posts->deleteAll(array('postId' => $del_post->postId))) {
//ファイル削除----- //3
foreach($del_imgs as $q) {
$file = new File(WWW_ROOT.'img/'.$q);
$file->delete();
$file = new File(WWW_ROOT.'img/mini/'.$q);
$file->delete();
}
//-----------------
$this->Flash->success(__('投稿が削除されました.'));
} else {
$this->Flash->error(__('投稿が削除されませんでした. もう一度お試し下さい.'));
}
return $this->redirect(['action' => 'index']);
}
else if ($this->Posts->delete($del_post)) {
//ファイル削除----- //4
$file = new File(WWW_ROOT.'img/'.$del_post->img_name);
$file->delete();
$file = new File(WWW_ROOT.'img/mini/'.$del_post->img_name);
$file->delete();
//-----------------
$this->Flash->success(__('投稿が削除されました.'));
} else {
$this->Flash->error(__('投稿が削除されませんでした. もう一度お試し下さい.'));
}
return $this->redirect($this->referer());
}
~~
似たような処理がいくつか散らばっているので、上手くリファクタリングしたいです。
とりあえず、以上で投稿を削除した際に、画像ファイルも一緒に削除できるようになりました。
#まとめ
とりあえず実装できましたが、
- サムネイル生成の処理がブサイク
- 削除アクションがブサイク
等問題があるので、何かアドバイスがありましたらコメント欄にてお願いいたしますm(__)m