【PHPでECサイト①】CRUD機能

## はじめに
今回は、PHPとMySQLを使ってMVCモデルでCRUD機能を実装していきます。

### CRUD機能とは？
ほぼ全てのソフトウェアが有する4つの永続的な基本機能の頭文字をそれぞれ並べた用語のこと。
その4つの機能とは、Create（生成）、Read（読み取り）、Update（更新）、Delete（削除）。

### バージョン
PHP：7.4.5
phpMyAdmin：5.0.2
MySQL：5.7.30

### 今回作成するファイル
conf / html / model / view の4つのディレクトリを作って進めます。

**conf(繰り返し使用するものを定義)**
- const.php

**html(controller)**
- admin.php
- admin_insert.php
- admin_update_stock.php
- admin_delete.php

**model**
- db.php
- functions.php
- items.php

**view**
- admin_view.php

## テーブルの作成

```sql:items.sql
CREATE TABLE `sample_items` (
    `item_id` int(11) NOT NULL,
    `name` varchar(100) NOT NULL,
    `price` int(11) NOT NULL,
    `stock` int(11) NOT NULL,
    `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)

ALTER TABLE `sample_items`
  MODIFY `item_id` int(11) NOT NULL AUTO_INCREMENT,
  ADD PRIMARY KEY (`item_id`);
```

## 定義

```php:const.php
<?php
// ディレクトリパス
define('MODEL_PATH', $_SERVER['DOCUMENT_ROOT'] . '/../model/');
define('VIEW_PATH', $_SERVER['DOCUMENT_ROOT'] . '/../view/');

// MySQL接続用
define('DB_HOST', 'ホスト名');
define('DB_NAME', 'データベース名');
define('DB_USER', 'ユーザ名');
define('DB_PASS', 'パスワード');
define('DB_CHARSET', 'utf8');

// URL
define('ADMIN_URL', '/admin.php');
```

## Viewの作成

```php:admin_view.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>商品管理</title>
  </head>

  <body>
    <h1>商品管理ページ</h1>

    <form method="post">
      <div>
        <label>名前</label>
        <input type="text" name="name">
      </div>
      <div>
        <label>価格</label>
        <input type="number" name="price">
      </div>
      <div>
        <label>在庫</label>
        <input type="number" name="stock">
      </div>
      <input type="submit" value="送信">
    </form>

    <!-- 表示 -->
    <table>
      <thead>
        <tr>
          <th>名前</th>
          <th>価格</th>
          <th>在庫</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </tbody>
    </table>
  </body>
</html>
```

## Create機能の実装

### Modelの作成

**db.phpを作成(データベース関係)**

```php:db.php
<?php

function get_db_connect(){
    // MySQL用のDSN文字列
    $dsn = 'mysql:dbname='. DB_NAME. ';host='. DB_HOST. ';charset='. DB_CHARSET;

    try{
      // DB接続
      $dbh = new PDO($dsn, DB_USER, DB_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'));
      $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
      $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    }catch(PDOException $e){
        exit('接続できませんでした。理由：'.$e->getMessage());
    }
    return $dbh;
}

function execute_query($db, $sql, $params = array()){
  try{
    $statement = $db->prepare($sql);
    return $statement->execute($params);
  }catch(PDOException $e){
    set_error('更新に失敗しました。');  
  }
  return false;
}
```

**functions.phpを作成(その他機能)**

```php:functions.php
<?php

function redirect_to($url){
  header('Location:'. $url);
  exit;
}

function get_post($name){
  if(isset($_POST[$name]) === true){
    return $_POST[$name];
  };
  return '';
}  

// セッション
function get_session($name){
  if(isset($_SESSION[$name]) === true){
      return $_SESSION[$name];
  };
  return '';
}

function set_session($name, $value){
  $_SESSION[$name] = $value;
}

// エラーメッセージ
function set_error($error){
  $_SESSION['__errors'][] = $error;
}

function get_errors(){
  $errors = get_session('__errors');
  if($errors === ''){
      return array();
  }
  set_session('__errors', array());
  return $errors;
}

// メッセージ
function set_message($message){
  $_SESSION['__message'][] = $message;
}

function get_messages(){
  $messages = get_session('__message');
  if($messages === ''){
      return array();
  }
  set_session('__message', array());
  return $messages;
}
```

**items.phpを作成(商品関係)**

```php:items.php
<?php
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'db.php';

// Create
function regist_item($db, $name, $price, $stock){
  $sql = "
    INSERT INTO
      sample_items(
        name,
        price,
        stock
      )
    VALUES(?,?,?)
  ";
  return execute_query($db, $sql, array($name, $price, $stock));
}
```

### Viewの作成
**admin_view.phpにCreate処理へ遷移するよう追記**
- 処理結果を表示できるよう追記
- formタグにactionを追記

```php:admin_view.php
<!-- 処理結果のメッセージ -->
<?php foreach(get_errors() as $error){ ?>
    <p><?php print $error; ?></p>
<?php } ?>
<?php foreach(get_messages() as $messages){ ?>
    <p><?php print $messages; ?></p>
<?php } ?>

<!-- 投稿フォーム -->
 <form method="post" action="admin_insert.php">
   <div>
     <label>名前</label>
     <input type="text" name="name">
   </div>
   <div>
     <label>価格</label>
     <input type="number" name="price">
   </div>
   <div>
     <label>在庫</label>
     <input type="number" name="stock">
   </div>
   <input type="submit" value="送信">
 </form>
```

### Controllerの作成
**admin.phpを作成(admin_view.php表示用)**

```php:admin.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'items.php';

session_start();

include_once VIEW_PATH. 'admin_view.php';
```

**admin_insert.phpを作成(Insert処理用)**

```php:bbs_insert.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$name = get_post('name');
$price = get_post('price');
$stock = get_post('stock');

// Insert
if(regist_item($db, $name, $price, $stock)){
    set_message('商品を登録しました。');
}else{
    set_error('商品の登録に失敗しました。');
}

redirect_to(ADMIN_URL);
```

## Read機能の実装
Createで挿入されたデータを表示されるようにしていきます。
### Modelの作成
**db.phpにRead機能を追記**

```php:db.php
function fetch_all_query($db, $sql, $params = array()){
  try{
    $statement = $db->prepare($sql);
    $statement->execute($params);
    return $statement->fetchAll();
  }catch(PDOException $e){
    set_error('データ取得に失敗しました。');
  }
  return false;
}
```

**items.phpに追記**

```php:items.php
// Read
function get_items($db){
  $sql = "
    SELECT
      item_id,
      name,
      price,
      stock
    FROM
      sample_items
  ";
  return fetch_all_query($db, $sql);
}
```

### Viewの作成
**admin_view.phpに追記**
- tableタグ内のtbodyタグに追記

```php:admin_view.php
<tbody>
  <?php foreach($items as $item){ ?>
  <tr>
    <td><?php print($item['name']); ?></td>
    <td><?php print($item['price']); ?></td>
    <td><?php print($item['stock']); ?></td>
    <td></td>
  </tr>
  <?php } ?>
</tbody>
```

### Controllerの作成

```php:admin.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'items.php';

session_start();

// SELECT文で選択したデータを表示させる為に追記
$db = get_db_connect();
$items = get_items($db);

include_once VIEW_PATH. 'admin_view.php';
```

## Update機能の実装
Createで生成されたデータを変更できるようにしていきます。

### Modelの作成
**items.phpにUpdate機能を追記**

```php:items.php
// Update
function update_stock($db, $item_id, $stock){
  $sql = "
    UPDATE
      sample_items
    SET
      stock = ?
    WHERE
      item_id = ?
  ";
  return execute_query($db, $sql, array($stock, $item_id));
}
```
### Viewの作成
**admin_view.phpにUpdate処理へ遷移するよう追記**
- tableタグ内のtbodyに追記

```php:bbs.php
<tbody>
  <?php foreach($items as $item){ ?>
  <tr>
    <td><?php print($item['name']); ?></td>
    <td><?php print($item['price']); ?></td>
    <td>
      <form method="post" action="admin_update_stock.php">
        <input type="number" name="stock" value="<?php print($item['stock']); ?>">
        <input type="submit" value="変更">
        <input type="hidden" name="item_id" value="<?php print($item['item_id']); ?>">
      </form>    
    </td>
    <td></td>
  </tr>
  <?php } ?>
</tbody>
```

### Controllerの作成
**admin_update_stock.phpを作成(Update処理用)**

```php:admin_update_stock.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'items.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$item_id = get_post('item_id');
$stock = get_post('stock');

// Update
if(update_stock($db, $item_id, $stock)){
    set_message('在庫を更新しました。');
  } else {
    set_error('在庫の更新に失敗しました。');
  }
  
redirect_to(ADMIN_URL);
```

## Delete機能の実装
Createで生成されたデータを削除できるようにしていきます。

### Modelの作成
**items.phpにDelete機能を追記**

```php:items.php
// Delete
function delete_item($db, $item_id){
  $sql = "
    DELETE FROM
      sample_items
    WHERE
      item_id = ?
  ";
  return execute_query($db, $sql, array($item_id));
}
```
### Viewの作成
**admin_view.phpにDelete処理へ遷移するよう追記**
- tableタグ内のtbodyに追記

```php:admin_view.php
<tbody>
  <?php foreach($items as $item){ ?>
  <tr>
    <td><?php print($item['name']); ?></td>
    <td><?php print($item['price']); ?></td>
    <td>
      <form method="post" action="admin_update_stock.php">
        <input type="number" name="stock" value="<?php print($item['stock']); ?>">
        <input type="submit" value="変更">
        <input type="hidden" name="item_id" value="<?php print($item['item_id']); ?>">
      </form>    
    </td>
    <td>
      <form method="post" action="admin_delete.php">
        <input type="submit" value="削除">
        <input type="hidden" name="item_id" value="<?php print($item['item_id']); ?>">
      </form>
    </td>
  </tr>
  <?php } ?>
</tbody>
```
### Controllerの作成
**admin_delete.phpを作成(Delete処理用)**

```php:bbs.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'items.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$item_id = get_post('item_id');

// Delete
if(delete_item($db, $item_id)){
    set_message('商品を削除しました。');
  } else {
    set_error('商品の削除に失敗しました。');
  }
  
redirect_to(ADMIN_URL);
```

## 参考
[CRUD機能と7つのアクションの関係性](https://qiita.com/okamoto_ryo/items/6bcc49cc0cbcb55f93da#:~:text=CRUD%E6%A9%9F%E8%83%BD%E3%81%A8%E3%81%AF%E3%80%81%E3%81%BB%E3%81%BC,Delete%EF%BC%88%E5%89%8A%E9%99%A4%EF%BC%89%E3%82%92%E6%8C%87%E3%81%99%E3%80%82)
[MVCモデルについて](https://qiita.com/s_emoto/items/975cc38a3e0de462966a)