Help us understand the problem. What is going on with this article?

CakePHPとAjaxでいいねボタンをつくりました

1週間前のAjax何もわかってない自分に向けて書きます。
CakePHP3.8を使っています。

つくりたいもの

Twitterのいいねボタンみたいなやつ。
掲示板の投稿メッセージ一覧に、メッセージに付随して表示する。
自分がボタンをすでに押している場合は、ボタンに色がついている。押していない場合、色がついていない。
他ユーザー含めて、いいねボタンが押された数を表示する。

まず

jQueryを勉強するところから始めました。

テーブルの構成

関係あるところらへんだけ

  • Users
    • id (int)
    • username(関係ない)
    • password(関係ない)
  • Posts
    • id (int)
    • message(関係ない)
  • Favorites
    • id (int)
    • user_id (int)
    • post_id (int)

とりあえずメッセージ一覧画面に、いいねボタンといいね数を表示する

関係ありそうなところだけ抜粋して載せています。
色々と省略しています。

PostsTable.php
public function initialize(array $config)
{
    // PostsとFavoritesは1対多
    $this->hasMany('Favorites', [
        'foreignKey' => 'post_id',
    ]);

}

// 投稿メッセージを一覧表示するのに使います
// メッセージごとのいいねの数も一緒に取得しています
public function findBbsPosts()
{
    $query = $this->find();
    $query
        ->contain(['Users', 'Favorites'])
        ->select(['favorites_count' => $query->func()->count('Favorites.id')])
        ->leftJoinWith('Favorites')
        ->group(['Posts.id'])
        ->enableAutoFields(true);

    return $query;
}
FavoritesTable.php
public function initialize(array $config)
{

    $this->belongsTo('Users', [
        'foreignKey' => 'user_id',
        'joinType' => 'INNER',
    ]);

    $this->belongsTo('Posts', [
        'foreignKey' => 'post_id',
        'joinType' => 'INNER',
    ]);
}

// 投稿メッセージごとのいいねの数をカウントするのに使います
public function countFavorite(int $post_id)
{
    $query = $this->find();
    $query
        ->where(['post_id' => $post_id]);
    return $query;
}

とりあえず投稿メッセージを一覧表示させます。
$authuserにログインしているユーザーについての情報を入れています。

PostsController.php
public function initialize()
{
    // 色々と省略

    $this->loadComponent('Paginator');

    $this->set('authuser', $this->Auth->user());
}

public function index()
{
    $bbsPosts = $this->paginate($this->Posts->findBbsPosts(), [
        'order' => ['Posts.id' => 'DESC'],
    ]);

    $this->set(compact('bbsPosts'));
}

投稿メッセージをelementに切り出しています。

Posts/index.ctp
<?php foreach ($bbsPosts as $bbsPost) : ?>
    <?= $this->element('article', ['bbsPost' => $bbsPost]) ?>
<?php endforeach; ?>

さらにarticle.ctpの中でもいいねボタンに関連する部分だけelementに切り出しています。

article.ctp
<?= $this->element('favorite', ['bbsPost' => $bbsPost]) ?>

すごく格好悪いのですが、array_searchを使って、「ログインしているユーザのID」と、「投稿メッセージごとに結合しているFavoritesのuser_id」で一致するものがあるかどうか探しにいきます。(効率が良くないです。)

  • 一致するものがあれば、favorite-add-buttonを非表示
  • 一致するものがなければ、favorite-delete-buttonを非表示
  • 非表示は、フォームを囲むdivのclassにhideを追加しています。

ハートマークの表示の違いは、Font Awesomeを使っています。
ボタンのclassのfas fa-heartfar fa-heartで変えています。

ついでに、フォームを囲むdivのid属性に、投稿メッセージごとのPostsのidをつけています。

favorite.ctp
<!-- まず、ログインユーザーが投稿ごとにいいねボタンを押しているかどうかを確認します -->
<?php $searchFavorite = array_search($authuser['id'], array_column($bbsPost->favorites, 'user_id'), true) ?>

<!-- 一致するものがなければ、まだ押していないので、「add」のフォームを表示 -->
<?php if ($searchFavorite === false) : ?>
    <div class="favorite" id="addfavorite<?php echo h($bbsPost->id) ?>">
<!-- 一致するものがあれば、すでに押しているので、「add」のフォームを非表示(hide追加) -->
<?php else : ?>
    <div class="favorite hide" id="addfavorite<?php echo h($bbsPost->id) ?>">
<?php endif; ?>

        <?= $this->Form->create('', [
            'url' => [
                'controller' => 'Favorites',
                'action' => 'add',
            ],
            'class' => 'favorite-form'
        ]) ?>
        <?php
            echo $this->Form->hidden('user_id', ['value' => $authuser['id']]);
            echo $this->Form->hidden('post_id', ['value' => $bbsPost->id]);
        ?>
        <?= $this->Form->button('', [
            'class' => 'far fa-heart favorite-add-button',
        ]) ?>
        <?= $this->Form->end() ?>
    </div>

<!-- 一致するものがあれば、すでに押しているので、「delete」のフォームを表示 -->
<?php if ($searchFavorite !== false) : ?>
    <div class="favorite" id="deletefavorite<?php echo h($bbsPost->id) ?>">
<!-- 一致するものがなければ、まだ押していないので、「delete」のフォームを非表示(hide追加) -->
<?php else : ?>
    <div class="favorite hide" id="deletefavorite<?php echo h($bbsPost->id) ?>">
<?php endif; ?>

        <?= $this->Form->create('', [
            'url' => [
                'controller' => 'Favorites',
                'action' => 'delete',
            ],
            'class' => 'favorite-form',
        ]) ?>
        <?php
            echo $this->Form->hidden('user_id', ['value' => $authuser['id']]);
            echo $this->Form->hidden('post_id', ['value' => $bbsPost->id]);
        ?>
        <?= $this->Form->button('', [
            'class' => 'fas fa-heart favorite-delete-button',
        ]) ?>
        <?= $this->Form->end() ?>
    </div>
css
.favorite.hide {
    display: none;
}

カウント部分はPostsTable.phpselect(['favorites_count' => $query->func()->count('Favorites.id')])として取得したものを使います。

favorite.ctp
<p class="favCount" id="favCount<?php echo h($bbsPost->id) ?>">
    <?php if (! isset($bbsPost->favorites_count)) : ?>
        0
    <?php else : ?>
        <?= $this->Number->format($bbsPost->favorites_count) ?>
    <?php endif; ?>
</p>

Ajaxを使ってハートマークとカウント数を変化させます

コントローラでデータベースに保存したり削除したりします。

FavoritesController.php
public function add()
{
    $this->autoRender = false;

    $favorite = $this->Favorites->newEntity();

    if ($this->request->is('ajax')) {
        $received_data = $this->request->getData();

        $favorite->user_id = $this->request->getData('user_id');
        $favorite->post_id = $this->request->getData('post_id');

        if ($this->Favorites->save($favorite)) {
            // FavoritesTable.phpに書いたcountFavorite()を使っていいねの数を取得します
            $count = $this->Favorites->countFavorite($favorite->post_id)->count();

            // favorite-button-change.jsに'received_data'と'count'を渡します
            $this->response->body(json_encode(['received_data' => $received_data, 'count' => $count]));
            return;
        }
        $this->Flash->error(__('The favorite could not be saved. Please, try again.'));
    }
}

public function delete()
{
    $this->autoRender = false;

    if ($this->request->is('ajax')) {
        $received_data = $this->request->getData();

        $param = [
            'user_id' => $this->request->getData('user_id'),
            'post_id' => $this->request->getData('post_id'),
        ];

        if ($this->Favorites->deleteAll($param)) {
            $count = $this->Favorites->countFavorite($this->request->getData('post_id'))->count();

            $this->response->body(json_encode(['received_data' => $received_data, 'count' => $count]));
            return;
        } else {
            $this->Flash->error(__('The favorite could not be deleted. Please, try again.'));
        }
    }
}
favorite-button-change.js
$(function () {
    // 「add」のボタンをクリックしたとき
    $('.favorite-add-button').click(function (event) {
        event.preventDefault();

        var param = $(this).parent('.favorite-form').serializeArray();

        $.ajax({
            url: '/bbs/Favorites/add', // FavoritesControllerのaddアクションに送ります
            type: 'POST',
            dataType: 'json',
            data: param,
            timeout: 10000,
        }).done(function (result) { // 成功の場合
            // FavoriteControllerのaddアクションのreturnの結果がresultに入っています
            // divのclass属性のhideを逆にします
            $('#addfavorite' + result['received_data']['post_id']).addClass('hide');
            $('#deletefavorite' + result['received_data']['post_id']).removeClass('hide');

            // いいね数を取得した数に書き換えます
            $('#favCount' + result['received_data']['post_id']).text(result['count']);
        }).fail(function (XMLHttpRequest, textStatus, errorThrown) { // 失敗の場合
            alert("失敗");
        });
    });

    // 「delete」のボタンをクリックしたとき
    $('.favorite-delete-button').click(function (event) {
        event.preventDefault();

        var param = $(this).parent('.favorite-form').serializeArray();

        $.ajax({
            url: '/bbs/Favorites/delete',
            type: 'POST',
            dataType: 'json',
            data: param,
            timeout: 10000,
        }).done(function (result) {
            $('#addfavorite' + result['received_data']['post_id']).removeClass('hide');
            $('#deletefavorite' + result['received_data']['post_id']).addClass('hide');
            $('#favCount' + result['received_data']['post_id']).text(result['count']);
        }).fail(function (XMLHttpRequest, textStatus, errorThrown) {
            alert("失敗");
        });
    });
});

拙い出来なので、ご指摘いただけるととてもありがたいです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした