17
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

CakePHP3で画像投稿機能付き掲示板作成 ~第三回 画像投稿機能の実装~

Last updated at Posted at 2016-03-21

第二回に引き続き、掲示板の作成を行っていきます。
今回は

  • 画像投稿機能の実装
    を行っていきます。

#目次
~第一回 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に設定しています
      • 画像をローカルに保存
    • ※サムネイル生成のところがややこしすぎるので、より良い方法を思案中
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'>のようにしています
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形式でローカルにある画像を表示することができました。
スクリーンショット 2016-03-21 19.14.28.png

#削除機能
投稿が削除された際に、画像も一緒に削除するようにdeleteアクションを編集します。
例によってブサイクなコードですが、以下解説です。

  1. 削除対象が返信投稿なのか大元の投稿なのかを判別します
  2. resID=0の場合、すべての返信投稿を削除するためにpostIdが一致する投稿の画像の名前を取得し、配列に保存しておきます
  3. foreachでまわしていき、オリジナルとサムネイルファイルを削除していきます
  4. 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

17
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?