7-6:商品管理画面を作成する

2019/05/29

概要

現状は商品一覧はスタティックにHTMLに記述して商品を表示しています。
管理画面で商品を登録して商品一覧に表示させられるようにしていきます。
そのために、この章では管理画面で商品の登録削除ができる機能を実装します。
また、登録した商品をフロント側の商品一覧に動的に表示させるように実装していきます。

フォルダ階層

corporate-site

adminフォルダ詳細

完成イメージ

商品登録

ダッシュボード

商品一覧

フロント側商品一覧

全体の手順

手順は以下のとおりです

  1. 商品テーブルの作成
  2. 商品登録機能の実装
  3. 管理画面に商品一覧表示
  4. フロント画面に商品一覧を動的に表示

商品テーブルの作成

商品を管理するために、データベースに商品テーブルを作成します。
商品管理のために必要な情報を考えてテーブルを作成します。
img_pathは画像を表示するときに必要な、画像のファイル名を登録するカラムです。
テーブル名はproductsとします。

項目 内容 オプション オプション
id ID int primary key auto_increment
product_name 商品名 varchar(64)
price 単価 int
text 説明文 varchar(10)
img_path 商品画像path varchar(256)
created_at 作成日時 datetime
updated_at 更新日時 dateimte

corporate_dbにproductsテーブルを作成します。
console

mysql> use corporate_db
Database changed
mysql> create table products(id int primary key auto_increment,product_name varchar(64),price int,text varchar(10),img_path varchar(256),created_at datetime,updated_at datetime); 
Query OK, 0 rows affected (0.08 sec)

Query OKが表示されればOKです。

商品登録機能の実装

登録フォーム作成

商品登録フォームを作成していきます。
管理機能なので、adminフォルダ内にファイルを作成します。
create_news.phpをコピーして、create_product.phpを作成します。
titleを「商品登録」に変更します。
<form>〜</form>の中は内容が全く変わるので一旦削除します。
formのactionとmethodも削除しておきます。
admin/create_product.php

<?php
    session_start();

    if($_SESSION['admin_login'] == false){
        header("Location:./index.html");
        exit;
    }

?>
<!DOCTYPE html>
<html>

<head>
    <!-- Global site tag (gtag.js) - Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-13xxxxxxxxx"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag() { dataLayer.push(arguments); }
        gtag('js', new Date());

        gtag('config', 'UA-13xxxxxxxxx');
    </script>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>商品登録</title>

    <link rel="icon" href="favicon.ico">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
        integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">

    <!-- css -->
    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <header>
        <div class="container">
            <div class="header-logo">
                <h1><a href="dashboard.php">管理画面</a></h1>
            </div>

            <nav class="menu-right menu">
                <a href="logout.php">ログアウト</a>
            </nav>
        </div>
    </header>
    <main>
        <div class="wrapper">
            <div class="container">
                <div class="wrapper-title">
                    <h3>新規作成</h3>
                </div>
                <form class="edit-form">

                </form>
            </div>
        </div>
    </main>
    <footer>
        <div class="container">
            <p>Copyright @ 2018 SQUARE, inc</p>
        </div>
    </footer>
</body>

</html>

削除した<form>〜</form>部分にデータベースのproductsテーブルの項目に合わせてフォームを作成します。
productsテーブルに保存するために必要な項目は、商品名・単価・説明文・商品画像pathです。
商品名・単価・説明文はinputタグのタイプ属性をtextにしました。また説明文はinputタグのmaxlength属性を使って8文字までに文字数制限をしました。inputタグの属性を使って他にもバリデーションを設定することができます。商品画像はファイルをアップロードするのでタイプ属性にfileを指定します。
admin/create_product.php

    <form class="edit-form">
+       <div class="form-group">
+           <p>商品名</p>
+           <input type="text" name="product_name" required>
+       </div>
+       <div class="form-group">
+           <p>説明文</p>
+           <input type="text" name="text" maxlength="8">
+       </div>
+       <div class="form-group">
+           <p>単価</p>
+           <input type="text" name="price" required>
+       </div>
+       <div class="form-group">
+           <p>アイテム画像</p>
+           <input type="file" name="img" class="imgform">
+       </div>
+       <button type="submit" class="btn btn-blue">登録</button>
    </form>

CSSも追加します。
admin/styles.css

/* products */
.edit-form .imgform {
    border: none;
}

フォームの送り先は、登録を実行するstore_product.phpにします。
忘れてはいけないのが、フォームタグにenctype="multipart/form-data"を付けることです。
画像に限らず、ファイルを送る場合はこのenctype="multipart/form-data"というおまじないをフォームに追加しなければいけません。
admin/create_product.php

+   <form class="edit-form" method="POST" action="store_product.php" enctype="multipart/form-data">
-   <form class="edit-form">

登録実装

アップロードフォルダの作成

画像をアップロードしたら保存するフォルダを作成します。
adminフォルダ内にproductsというフォルダを作成します。

実行ファイル作成

登録を実行するstore_product.phpを作成します。
adminフォルダ内にstore_product.phpを作成します。

ログインチェック

admin/store_product.php

<?php

    //sessionでログイン制限
    session_start();

    if($_SESSION['admin_login'] == false){
        header("Location:./index.html");
        exit;
    }

データ受け取り

一旦、画像以外のデータをいつもどおりに受け取ります。
説明文に関しては改行コードの処理を入れています。
admin/store_product.php

    <?php

        //sessionでログイン制限
        session_start();

        if($_SESSION['admin_login'] == false){
            header("Location:./index.html");
            exit;
        }

+      //値受け取り
+      $product_name = isset($_POST['product_name'])? htmlspecialchars($_POST['product_name'], ENT_QUOTES, 'utf-8'):'';
+      $text = isset($_POST['text'])? htmlspecialchars($_POST['text'], ENT_QUOTES, 'utf-8'):'';
+      $text = nl2br($text);
+      $price = isset($_POST['price'])? htmlspecialchars($_POST['price'], ENT_QUOTES, 'utf-8'):''; 

画像アップロード(分岐1)

HTTP POSTによりアップロードされたファイルかどうかを調べます。
NGだった場合、「何らからの攻撃をうけました」と表示させるようにしました。
admin/store_product.php


   //値受け取り
   $product_name = isset($_POST['product_name'])? htmlspecialchars($_POST['product_name'], ENT_QUOTES, 'utf-8'):'';
   $text = isset($_POST['text'])? htmlspecialchars($_POST['text'], ENT_QUOTES, 'utf-8'):'';
   $text = nl2br($text);
   $price = isset($_POST['price'])? htmlspecialchars($_POST['price'], ENT_QUOTES, 'utf-8'):'';

+   if (is_uploaded_file($_FILES["img"]["tmp_name"])) {
+       //HTTP POST OK;
+   } else {
+       echo "何らからの攻撃をうけました";
+       exit;
+   }

画像アップロード(分岐2)

分岐1がOKだった場合、ファイルの拡張子のチェックをします。
アップロードしようとしているファイルの名前を取得して、画像ファイルではないもののアップロードを防ぎます。
今回はjpg/pngのみ受け付けます。
NGだった場合、「ファイル形式はjpg/pngのみです」と表示させるようにしました。
admin/store_product.php

    if (is_uploaded_file($_FILES["img"]["tmp_name"])) {
        //HTTP POST OK;

+       $file_name = $_FILES["img"]["name"];
+
+       if (pathinfo($file_name, PATHINFO_EXTENSION) == 'jpg' || pathinfo($file_name, PATHINFO_EXTENSION) == 'png') {
+           //拡張子OK
+       } else {
+           echo "ファイル形式はjpg/pngのみです";
+           exit;
+       }

    } else {
        echo "何らからの攻撃をうけました";
        exit;
    }

このとき、ファイル名が全く同じファイルをアップロードすると上書きされてしまうので、ファイル名を取得したときに、ファイル名の頭にアップロード日時を追加して一意なデータにします。
admin/store_product.php

-       $file_name = $_FILES["img"]["name"];
+       $file_name = date('YmdHis')."_".$_FILES["img"]["name"];

画像アップロード(分岐3)

分岐2がOKだったら、ファイルを指定の場所(admin/productsフォルダ)に移動させます。
デフォルトでサーバ上の$_FILES["img"]["tmp_name"]にアップロードされているのをmove_uploaded_file(元の場所,指定場所)で移動させます。
NGだった場合「画像をアップロードできません。」と表示させるようにしました。
admin/store_product.php


    if (is_uploaded_file($_FILES["img"]["tmp_name"])) {
        //HTTP POST OK;

        $file_name = date('YmdHis')."_".$_FILES["img"]["name"];

        if (pathinfo($file_name, PATHINFO_EXTENSION) == 'jpg' || pathinfo($file_name, PATHINFO_EXTENSION) == 'png') {
             //拡張子OK
+           //元のアップロード先
+           $file_tmp_name = $_FILES["img"]["tmp_name"];
+           if (move_uploaded_file($file_tmp_name, "./products/" . $file_name)) {
+               //アップロード完了
+               echo "アップロード完了";
+           } else {
+               echo "画像をアップロードできません。";
+               exit;
+           }

        } else {
            echo "ファイル形式はjpg/pngのみです";
            exit;
        }

    } else {
        echo "何らからの攻撃をうけました";
        exit;
    }

確認

一旦確認してみます。
admin/create_product.phpからファイルをアップロードさせてみます。
ブラウザにアップロード完了と表示されることを確認します。

admin/productsフォルダを実際にみてみます。
admin/productsフォルダ内にアップロードさせたい画像が入っていれば画像アップロード機能はOKです。

productsテーブルに登録

画像がアップロードできたら、productsテーブルにデータを登録します。
DBに接続してINSERT文で登録します。
admin/store_product.php

if (move_uploaded_file($file_tmp_name, "./products/" . $file_name)) {
    //アップロード完了
-   echo "アップロード完了";    

+   //DB接続
+   try{
+       $dbh = new PDO("mysql:host=localhost;dbname=corporate_db","root","root");
+   }catch(PDOException $e){
+       var_dump($e->getMessage());
+       exit;
+   }

+   $stmt = $dbh->prepare("INSERT INTO products(
+       product_name,
+       text,
+       price,
+       img_path,
+       created_at,
+       updated_at
+   ) VALUES(
+       :product_name,
+       :text,
+       :price,
+       :img_path,
+       now(),
+       now()
+   )");
+   $stmt->bindParam(':product_name',$product_name);
+   $stmt->bindParam(':text',$text);
+   $stmt->bindParam(':price',$price);
+   $stmt->bindParam(':img_path',$file_name);
+   $stmt->execute();

+      echo "登録完了";    

} else {
    echo "画像をアップロードできません。";
    exit;
}

確認

動作確認してみます。
登録フォームから登録を実行し、「登録完了」と表示されたことを確認します。

次にデータベースを確認します。
console

mysql> select * from products;
+----+--------------+-------+-----------------+------------+---------------------+---------------------+
| id | product_name | price | text            | img_path   | created_at          | updated_at          |
+----+--------------+-------+-----------------+------------+---------------------+---------------------+
|  1 | テスト       |   300 | テスト商品      | banana.jpg | 2019-05-29 21:00:59 | 2019-05-29 21:00:59 |
+----+--------------+-------+-----------------+------------+---------------------+---------------------+
1 row in set (0.00 sec)

登録内容がきちんと入っていればOKです。

リダイレクト設定

登録が完了したら、商品一覧画面にリダイレクトさせるように設定します。
これから作成するのでまだファイルはありませんが、products.phpにリダイレクトさせるようにします。
admin/store_product.php

-   echo "登録完了";
+   header('location:./products.php');

一覧表示

dashboard追加

ダッシュボードに商品管理を追加します。
admin/dashboard.php

    <div class="boxs">
        <a href="news.php" class="box">
            <i class="far fa-newspaper icon"></i>
            <p>記事管理</p>
        </a>
        <a href="users.php" class="box">
            <i class="fas fa-users icon"></i>
            <p>会員管理</p>
        </a>
        <a href="orders.php" class="box">
            <i class="fas fa-ambulance icon"></i>
            <p>受注管理</p>
        </a>
+       <a href="products.php" class="box">
+           <i class="fas fa-store-alt icon"></i>
+           <p>商品管理</p>
+       </a>
    </div>

確認します。

一覧画面作成

一覧表示画面を作成します。
news.phpをコピーして、products.phpを作成します。
php部分をproductsテーブルから取得するように変更します。
admin/products.php

    <?php

        //sessionでログイン制限
        session_start();

        if($_SESSION['admin_login'] == false){
            header("Location:./index.html");
            exit;
        }

        //DB接続
        try{
            $dbh = new PDO("mysql:host=localhost;dbname=corporate_db","root","root");
        }catch(PDOException $e){
            var_dump($e->getMessage());
            exit;
        }

+      $stmt = $dbh->prepare("SELECT * FROM products");
-      $stmt = $dbh->prepare("SELECT * FROM news");
        $stmt->execute();
+      $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      $news = $stmt->fetchAll(PDO::FETCH_ASSOC);
    ?>

title・h3タグを「商品管理」に変更します。
投稿するボタンもcreate_product.phpへのリンクに変更し「商品登録する」にします。
表示方法はnews.phpと同じなので、消さずにproducts用に変更していきます。

admin/products.php

    <!DOCTYPE html>
    <html>
        <head>
            <!-- Global site tag (gtag.js) - Google Analytics -->
            <script async src="https://www.googletagmanager.com/gtag/js?id=UA-13xxxxxxxxx"></script>
            <script>
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());

                gtag('config', 'UA-13xxxxxxxxx');
            </script>

            <meta charset="utf-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">

-          <title>記事管理</title>
+          <title>商品管理</title>

        <link rel="icon" href="favicon.ico">
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">

        <!-- css -->
        <link rel="stylesheet" href="./styles.css">
    </head>
    <body>
        <header>
            <div class="container">
                <div class="header-logo">
                    <h1><a href="dashboard.php">管理画面</a></h1>
                </div>

                <nav class="menu-right menu">
                    <a href="logout.php">ログアウト</a>
                </nav>
            </div>
        </header>
        <main>
            <div class="wrapper">
                <div class="container">
                    <div class="wrapper-title">
-                          <h3>記事管理</h3>
+                          <h3>商品管理</h3>
                    </div>
-                       <button class="btn btn-blue" onclick="location.href='create_news.php'">投稿する</button>
+                       <button class="btn btn-blue" onclick="location.href='create_product.php'">商品登録する</button>
                    <div class="list">
                        <table>
                            <thead>
                                <tr>
                                    <th>id</th>
-                                      <th>タイトル</th>
-                                      <th>本文</th>
+                                      <th>商品名</th>
+                                      <th>説明文</th>
+                                      <th>単価</th>
+                                      <th>画像パス</th>
                                    <th>更新日時</th>
                                    <th>作成日時</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody>
-                                   <?php foreach($news as $new): ?>
+                                   <?php foreach($products as $product): ?>
                                <tr>
-                                      <td><?php echo $new['id']; ?></td>
-                                      <td><?php echo $new['title']; ?></td>
-                                      <td><?php echo $new['content']; ?></td>
-                                      <td><?php echo $new['created_at']; ?></td>
-                                      <td><?php echo $new['updated_at']; ?></td>
+                                      <td><?php echo $product['id']; ?></td>
+                                      <td><?php echo $product['product_name']; ?></td>
+                                      <td><?php echo $product['text']; ?></td>
+                                      <td><?php echo $product['price']; ?></td>
+                                      <td><?php echo $product['img_path']; ?></td>
+                                      <td><?php echo $product['updated_at']; ?></td>
+                                      <td><?php echo $product['created_at']; ?></td>
                                   <td>
-                                           <button class="btn btn-green" onclick="location.href='edit_news.php?id=<?php echo $new['id']; ?>'">編集</button>
-                                           <button class="btn btn-red delete" data-id=<?php echo $new['id']; ?>>削除</button>
-                                           <form method="POST" action="./delete_news.php" id="delete_form_<?php echo $new['id']; ?>">
-                                               <input type="hidden" value="<?php echo $new['id']; ?>" name="id">
-                                           </form>
+                                           <button class="btn btn-green">編集</button>
+                                           <button class="btn btn-red" >削除</button>
                                    </td>
                                </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </main>
        <footer>
            <div class="container">
                <p>Copyright @ 2018 SQUARE, inc</p>
            </div>
        </footer>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    </body>
</html>

確認

dashboradから商品管理を確認します。

このように表示されていればOKです。

※編集・削除機能の実装とページング等は、割愛しています。
Newsの更新を簡易CMS化する2:NewsのCRUD機能の実装会員機能を追加する2:ユーザー管理機能を作成するを参考に実装してみてください。

これで商品管理画面の作成が完了しました。

フロント画面商品一覧の表示

商品を表示させるために、管理画面のcreate_product.phpから商品を数件登録しておきます。

データベースの連携

shop.phpを編集していきます。
DBに接続して表示するようにします。
いつものようにSELECT文の結果は$productsという名前で配列に入れて保存したいのですが、今回は$product_listとします。
なぜ$productsではダメかというと、$productsという変数は既にsessionで定義されているからです。
$productsにSELECTの結果を入れても、その後のsessionで$products$_SESSION['products']の結果を入れているので、上書きされてしまい、SELECTの結果を$productsで取得することができなくなってしまいます。

※上記は名前がかぶってしまった場合のイメージです
なので、変数名が被らないように$product_listに格納します。
shop.php

    <?php

+      try{
+          $dbh = new PDO("mysql:host=localhost;dbname=corporate_db","root","root");
+      }catch(PDOException $e){
+          var_dump($e->getMessage());
+          exit;
+      }

+      $stmt = $dbh->prepare("SELECT * FROM products");
+      $stmt->execute();
+      $product_list = $stmt->fetchAll(PDO::FETCH_ASSOC);

       $name = isset($_POST['name'])? htmlspecialchars($_POST['name'], ENT_QUOTES, 'utf-8') : '';
       $price = isset($_POST['price'])? htmlspecialchars($_POST['price'], ENT_QUOTES, 'utf-8') : '';
       $count = isset($_POST['count'])? htmlspecialchars($_POST['count'], ENT_QUOTES, 'utf-8') : '';

表示する

スタティックで記述していた部分を編集してループで回したデータを表示するようにします。

不要なコード削除

<div class="itemlist"><ul>内の一番最初の<li>〜</li>だけ残して、スタティックに記述しているコードを削除します。
下記のようなスッキリした状態にします。
shop.php

    <main>
        <div class="breadcrumbs">
            <div class="container">
                <ul>
                    <li><a href="index.php">TOP</a></li>
                    <li>商品一覧</li>
                </ul>
            </div>
        </div>
        <div class="wrapper last-wrapper">
            <div class="container">
                <div class="wrapper-title">
                    <h3>SHOP</h3>
                    <p>商品一覧</p>
                </div>
                <div class="itemlist">
                    <ul>
                        <li>
                            <img src="products/banana.jpg" >
                            <div class="item-body">
                                <h5>バナナ</h5>
                                <p>¥500</p>
                                <form action="shop.php" method="POST" class="item-form">
                                    <input type="hidden" name="name" value="バナナ">
                                    <input type="hidden" name="price" value="500">
                                    <input type="text" value="1" name="count">
                                    <button type="submit" class="btn-sm btn-blue">カートに入れる</button>
                                </form>
                            </div><!-- end item-body--> 
                        </li>
                    </ul>
                </div><!-- end itemlist -->
            </div>
        </div>
    </main>

今残っている、<li>をループで回して中身を動的に作成していきます。
画像は、今までproductsフォルダの画像を表示していましたが、adminフォルダ内のproductsフォルダに画像を保存するようにしたのでパスが変わります。
shop.php

    <main>
        <div class="breadcrumbs">
            <div class="container">
                <ul>
                    <li><a href="index.php">TOP</a></li>
                    <li>商品一覧</li>
                </ul>
            </div>
        </div>
        <div class="wrapper last-wrapper">
            <div class="container">
                <div class="wrapper-title">
                    <h3>SHOP</h3>
                    <p>商品一覧</p>
                </div>
                <div class="itemlist">
                    <ul>
+                       <?php foreach($product_list as $product): ?>
                        <li>
-                           <img src="products/banana.jpg" >
+                           <img src="admin/products/<?php echo $product['img_path']; ?>" >
                            <div class="item-body">
-                               <h5>バナナ</h5>
-                               <p>¥500</p>
+                               <h5><?php echo $product['product_name']; ?></h5>
+                               <p><?php echo $product['text']; ?></p>
+                               <p>¥<?php echo $product['price']; ?></p>
                                <form action="shop.php" method="POST" class="item-form">
-                                   <input type="hidden" name="name" value="バナナ">
-                                   <input type="hidden" name="price" value="500">
+                                   <input type="hidden" name="name" value="<?php echo $product['product_name']; ?>">
+                                   <input type="hidden" name="price" value="<?php echo $product['price']; ?>">
                                    <input type="text" value="1" name="count">
                                    <button type="submit" class="btn-sm btn-blue">カートに入れる</button>
                                </form>
                            </div><!-- end item-body--> 
                        </li>
+                       <?php endforeach; ?>
                    </ul>
                </div><!-- end itemlist -->
            </div>
        </div>
    </main>

確認する

shop.phpにアクセスして動作確認してください。
管理画面で登録したデータが反映されて、カートに入れる機能も今までどおり動いていればOKです。

これで商品の動的表示が完了しました。

コード

https://github.com/bluecode-io/web-basic/tree/basic7-6