PHPで【いいねボタン】を実装してみた

#最初に
現在通っているプログラミングスクールのPHPカリキュラムの中で教材（市販の入門書）に沿って簡易掲示板を作成。
その簡易掲示板を元に自分で考えて【いいねボタン】機能を追加してみました。

学習後のアウトプットを目的としており、教材の入門書一冊分の知識だけです。
フレームワークは使っていません。
元となる書籍の内容には触れていませんので、同じ書籍を見ていない人には分かりづらいと思います。
きっと…いや、間違いなくもっと綺麗なコードがあるはずですが、同じ初学者の方の参考になれば幸いです。


※教材はこちらです→[よくわかるPHPの教科書 【PHP7対応版】](https://www.amazon.co.jp/gp/product/4839964688/ref=dbs_a_def_rwt_bibl_vppi_i1)

#完成イメージ
今回はいいねボタンを簡単なハート文字にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/ea81c5be-40ba-b128-2842-046c30eb4eeb.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/4309bc19-8e0a-9c09-c86c-e97a3ab903c7.png)


#DB

既存のテーブルはこちら

**membersテーブル**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/ba3045c0-e1a4-f325-5ccc-7dae790de355.png)

**postsテーブル**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/125700b6-4774-bfc8-eb49-d1ff6adf5f8a.png)

【いいねボタン】機能のために追加したテーブル

**likesテーブル**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/c85ab8f5-e3ab-d425-e396-829cc002b832.png)

#1. いいねボタンをクリックした時にDBにデータを挿入or削除する

###1-1. いいねボタン（仮）を用意する

```php:index.php
<a class="heart" href="index.php?like=<?php echo h($post['id']); ?>">&#9825;</a>
```
表示部分です。
URLパラメータのlikeに`$post['id']`を使います。
※`$post['id']`はpostsテーブルのidを取得した値になっています。

いいねボタンをクリックすると`$_REQUEST['LIKE']`に`$post['id']の値`が入ります。

###1-2. いいねを押したメッセージの投稿者を調べる

```php:index.php
if (isset($_REQUEST['like'])) {

  //いいねを押したメッセージの投稿者を調べる
  $contributor = $db->prepare('SELECT member_id FROM posts WHERE id=?');
  $contributor->execute(array($_REQUEST['like']));
  $pressed_message = $contributor->fetch();
```
`isset()`で①の`$_REQUEST['LIKE']`に値が入ったか確認しtrueであれば、いいねを押したメッセージの投稿者を調べます。

###1-3. いいねを押した人とメッセージ投稿者が同一人物でないか確認

```php:index.php
//いいねを押した人とメッセージ投稿者が同一人物でないか確認
  if ($_SESSION['id'] != $pressed_message['member_id']) {
```
※`$_SESSION['id']`はログインした人のmembersテーブルidの値が入っています。

ここでログインしている人が自分の投稿にはいいねを押せないようにします。

###1-4. 過去にいいね済みであるか確認
```php:index.php

    //過去にいいね済みであるか確認
    $pressed = $db->prepare('SELECT COUNT(*) AS cnt FROM likes WHERE post_id=? AND member_id=?');
    $pressed->execute(array(
      $_REQUEST['like'],
      $_SESSION['id']
    ));
    $my_like_cnt = $pressed->fetch();
```
likeテーブルにログイン者が同じメッセージにいいねしてあるかCOUNTで確認。

###1-5. いいねのデータを挿入or削除
```php:index.php
//いいねのデータを挿入or削除
    if ($my_like_cnt['cnt'] < 1) {
      $press = $db->prepare('INSERT INTO likes SET post_id=?, member_id=?, created=NOW()');
      $press->execute(array(
        $_REQUEST['like'],
        $_SESSION['id']
      ));
      header("Location: index.php");
      exit();
    } else {
      $cancel = $db->prepare('DELETE FROM likes WHERE post_id=? AND member_id=?');
      $cancel->execute(array(
        $_REQUEST['like'],
        $_SESSION['id']
      ));
      header("Location: index.php");
      exit();
    }
```
④で調べた`$my_like_cnt['cnt']`の値で分岐させて、挿入か削除をする。

これでいいねボタンを1回クリックするとlikeテーブルにデータが挿入され、もう一度クリックすると今度は削除されます。(DBで確認しましょう）



#2. いいねボタンのハートの表示を変化させる

###2-1. ログインしている人がいいねしたメッセージをすべて取得
```php:index.php
//ログインしている人がいいねしたメッセージをすべて取得
$like = $db->prepare('SELECT post_id FROM likes WHERE member_id=?');
$like->execute(array($_SESSION['id']));
while ($like_record = $like->fetch()) {
  $my_like[] = $like_record;
}
```
ここは取り出すレコードが複数になることがあるので、while文で一つずつ値を取り出します。
取り出した値はもともと配列になっているので、さらに配列（二次元配列）として変数`$my_like[]`に代入します。

```php:
//$my_likeの中身（例）

     Array(
      [0] => Array(
            [post_id] => 17
            [0] => 17
            )
      [1] => Array(
            [post_id] => 19
            [0] => 19
            )
```
###2-2. それぞれの投稿されたメッセージが、ログインしている人がいいねしたメッセージに当てはまるかを調べる
```php:index.php
      $my_like_cnt = 0;
       if (!empty($my_like)) {
        foreach ($my_like as $like_post) {
         foreach ($like_post as $like_post_id) {
          if ($like_post_id == $post['id']) {
           $my_like_cnt = 1;
          }
         }
        }
```
テキストではもともとforeach文でメッセージ全てを表示させていますが（$postがその変数）、その中で上記のコードを入れます。
まず、繰り返されるごとに`$my_like_cnt`の値を初期化するために０を代入します。
`$my_like`は二次元配列なので、foreachをネストして値を取り出します。
`$like_post_id`（いいねしたことのあるメッセージ）と`$post['id']`（表示しているメッセージ）が同一であれば、`$my_like_cnt`に１を代入します。

※`$my_like_cnt`に代入している数字自体に特に意味はありません。

###2-3. いいねボタンのハート表示を切り替える
```php:index.php
<?php if ($my_like_cnt < 1) : ?>
<a class="heart" href="index.php?like=<?php echo h($post['id']); ?>">&#9825;</a>
<?php else : ?>
<a class="heart red" href="index.php?like=<?php echo h($post['id']); ?>">&#9829;</a>
<?php endif; ?>
```
2-2の結果をif文で分岐させていいねボタンの表示を切り替えます。

※ここではCSSは省略しますので、お好きにどうぞ。

これでいいねボタンの表示を変化させることが出来ました。

#3. メッセージごとにいいねされた件数を表示する
###3-1. メッセージ別のいいねされた件数をDBから取り出す
```php:index.php
$posts = $db->prepare('SELECT m.name, m.picture, p.*, COUNT(l.post_id) AS like_cnt FROM members m, posts p LEFT JOIN likes l ON p.id=l.post_id WHERE m.id=p.member_id GROUP BY l.post_id ORDER BY p.created DESC LIMIT ?, 5');
$posts->bindParam(1, $start, PDO::PARAM_INT);
$posts->execute();
```
元の投稿内容を取得するところで、一緒にlikeテーブルからも取得するように書き換えます。
ここで追加で取得したいのは、メッセージ別のいいねされた件数です。それが`COUNT(l.post_id) AS like_cnt`になります。`LEFT JOIN likes l ON p.id=l.post_id`としてリレーションを張って、`GROUP BY l.post_id`とすることでOKです。


###3-2. いいねボタンの横に表示させる
```php:index.php
<span><?php echo h($post['like_cnt']); ?></span>
```
これでメッセージごとにいいねされた件数を表示することが出来ました。

#4. いいねボタンをクリックするたびに1ページ目に戻らないようにする
```php:index.php
<?php if ($my_like_cnt < 1) : ?>
<a class="heart" href="index.php?like=<?php echo h($post['id']); ?>&page=<?php echo h($page); ?>">&#9825;</a>
<?php else : ?>
<a class="heart red" href="index.php?like=<?php echo h($post['id']); ?>&page=<?php echo h($page); ?>">&#9829;</a>
<?php endif; ?>
```
2-3の部分に`&page=<?php echo h($page); ?>`を追加して上記のように修正する。
そうするとURLパラメータに現在のページ$pageを渡すので、1-5のheaderファンクションの部分を下記のように書き直します。

```php:index.php
header("Location: index.php?page={$page}");
```

これでいいねボタンをクリックしたあとも元のページが表示されます。

#まとめ
```php:index.php
//3-1. メッセージ別のいいねされた件数をDBから取り出す
$posts = $db->prepare('SELECT m.name, m.picture, p.*, COUNT(l.post_id) AS like_cnt FROM members m, posts p LEFT JOIN likes l ON p.id=l.post_id WHERE m.id=p.member_id GROUP BY l.post_id ORDER BY p.created DESC LIMIT ?, 5');
$posts->bindParam(1, $start, PDO::PARAM_INT);
$posts->execute();

//1-2. いいねボタン
if (isset($_REQUEST['like'])) {

  //いいねを押したメッセージの投稿者を調べる
  $contributor = $db->prepare('SELECT member_id FROM posts WHERE id=?');
  $contributor->execute(array($_REQUEST['like']));
  $pressed_message = $contributor->fetch();

  //1-3. いいねを押した人とメッセージ投稿者が同一人物でないか確認
  if ($_SESSION['id'] != $pressed_message['member_id']) {

    //1-4. 過去にいいね済みであるか確認
    $pressed = $db->prepare('SELECT COUNT(*) AS cnt FROM likes WHERE post_id=? AND member_id=?');
    $pressed->execute(array(
      $_REQUEST['like'],
      $_SESSION['id']
    ));
    $my_like_cnt = $pressed->fetch();

    //1-5. いいねのデータを挿入or削除
    if ($my_like_cnt['cnt'] < 1) {
      $press = $db->prepare('INSERT INTO likes SET post_id=?, member_id=?, created=NOW()');
      $press->execute(array(
        $_REQUEST['like'],
        $_SESSION['id']
      ));
      header("Location: index.php?page={$page}");
      exit();
    } else {
      $cancel = $db->prepare('DELETE FROM likes WHERE post_id=? AND member_id=?');
      $cancel->execute(array(
        $_REQUEST['like'],
        $login_user
      ));
      header("Location: index.php?page={$page}");
      exit();
    }
  }
}

//2-1. ログインしている人がいいねしたメッセージをすべて取得
$like = $db->prepare('SELECT post_id FROM likes WHERE member_id=?');
$like->execute(array($_SESSION['id']));
while ($like_record = $like->fetch()) {
  $my_like[] = $like_record;
}
```
```php:index.php
<?php
  $my_like_cnt = 0;
  if (!empty($my_like)) {
    foreach ($my_like as $like_post) {
      foreach ($like_post as $like_post_id) {
        if ($like_post_id == $post['id']) {
          $my_like_cnt = 1;
        }
      }
    }
  }
  ?>
<?php if ($my_like_cnt < 1) : ?>
  <a class="heart" href="index.php?like=<?php echo h($post['id']); ?>&page=<?php echo h($page); ?>">&#9825;</a>
<?php else : ?>
  <a class="heart red" href="index.php?like=<?php echo h($post['id']); ?>&page=<?php echo h($page); ?>">&#9829;</a>
<?php endif; ?>
<span><?php echo h($post['like_cnt']); ?></span>
```

#完成した表示

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/2c3cd0ae-2a7f-20e9-83a1-7d6dfb28f5b1.png)

ハートをクリックすると赤くなってカウンターが増えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/ab8c4e02-6926-98a2-9bfa-4cdf3daed620.png)

もう一度クリックすると色が戻ってカウンターが減ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323137/2c3cd0ae-2a7f-20e9-83a1-7d6dfb28f5b1.png)

#振り返り
今回の実装にあたり個人的に苦戦したのは、
①DB設計
新たなテーブルにどんなカラムを用意すれば良いのか、テーブル間のリレーションはどうなるのか。
②SQL文
3-1では3つのテーブルでリレーションを張ってというような感じで、少し複雑になると思うようにデータを取り出すことが出来ず、解決まで時間がかかった。
③二次元配列
2-1、2-2では二次元配列の扱い方について理解が不十分だった為、時間がかかった。


きっと実務では、Ajaxで非同期処理をしたり、それぞれの処理をクラスで分けたり、色々あると思いますが、少しずつレベルアップして行きたいと思います。