Chapter19 管理画面と削除機能

概要と目標 管理画面を作成し、
記事を削除できる機能を作ろう。

管理画面に記事の一覧を表示し、記事を削除できるようになりましょう。

今回のゴール

RERUN

管理画面の作成 サイト管理者用のトップページを作成。

サイトの管理者が記事を管理するためのページを作成。

管理画面の作成しよう。

  1. 「mini-cms」 › 「admin」フォルダ内に「index.php」を作成
  2. データベースから全ての記事を取得し、 talbe要素で表示
mini-cms/admin/index.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'SELECT * FROM posts';

    // SQLを実行
    $stmt = $dbh->query($sql);

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // print_r($result);

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>管理画面</title>
</head>
<body>
  <h1>管理画面</h1>
  <p><a href="post.php">新しい記事を投稿する</a></p>

  <table border="1">
    <thead>
      <tr>
        <th>ID</th>
        <th>タイトル</th>
        <th>公開日</th>
        <th>更新日</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach($result as $row) : ?>
      <tr>
        <td><?php echo h($row['id']); ?></td>
        <td><?php echo h($row['title']); ?></td>
        <td><time datetime="<?php echo h($row['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['created']))); ?></time></td>
        <td><time datetime="<?php echo h($row['modified']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['modified']))); ?></time></td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
</body>
</html>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
ブラウザでの表示例

RERUN

削除ボタンの追加 記事のIDを埋め込んだ
「削除」ボタンを追加する。

テーブルの列に「削除」ボタンを追加する。
その際、form要素内に記事のIDをinput要素のhidden属性で埋め込んでおく。

削除ボタンの追加してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. 各レコードの最後の列に「削除」と文字を追加し、
    記事のIDを隠しフィールドで埋め込んだ「delete.php」へのform要素を設定
mini-cms/admin/index.php
<table border="1">
  <thead>
    <tr>
      <th>ID</th>
      <th>タイトル</th>
      <th>公開日</th>
      <th>更新日</th>
      <th>削除</th>
    </tr>
  </thead>
  <tbody>
    <?php foreach($result as $row) : ?>
    <tr>
      <td><?php echo h($row['id']); ?></td>
      <td><?php echo h($row['title']); ?></td>
      <td><time datetime="<?php echo h($row['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['created']))); ?></time></td>
      <td><time datetime="<?php echo h($row['modified']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['modified']))); ?></time></td>
      <td>
        <form action="delete.php" method="post">
          <input type="hidden" name="id" value="<?php echo h($row['id']); ?>">
          <input type="submit" value="削除">
        </form>
      </td>
    </tr>
    <?php endforeach; ?>
  </tbody>
</table>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 記事IDの隠しフィールドが埋め込まれた
    「削除」ボタンができているを確認
ブラウザでの表示例

RERUN

削除機能 隠しフィールドの記事IDで、
メッセージを削除する。

削除処理は隠しフィールドで送信した記事IDと一致するメッセージを
DELETE文で削除する。

削除機能を作成してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内に「delete.php」を作成
  2. リクエストメソッドがPOSTじゃなかったら管理画面にリダイレクト
    POSTだったら、記事IDを使って記事を削除し、その旨のメッセージを表示
mini-cms/admin/delete.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // POSTかダイレクトかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
    // POSTじゃない場合
    header('Location: index.php');
    exit();
  }

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'DELETE FROM posts WHERE id = ?';

    // ステートメント用意
    $stmt = $dbh->prepare($sql);

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$_POST['id'] , PDO::PARAM_INT);

    // ステートメントを実行
    $stmt->execute();

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>記事の削除</title>
</head>
<body>
  <h1>記事の削除</h1>
  <p>記事を削除しました。</p>

  <p><a href="./">一覧に戻る</a></p>
</body>
</html>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 記事が削除できるかを確認
ブラウザでの表示例

RERUN

CSRF対策 外部のフォームから
削除されないように。

このままでは、外部のフォームからでも記事を削除できてしまう。
外部のフォームから削除されないように、CSRF対策を行う。

CSRF対策をやってみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. セッションを開始後、 作成した独自関数を利用してトークンをセット
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // CSRF対策 ・・・ トークンの生成
  set_token();

答えを見る

  1. <input type="hidden">を使って、 トークンを埋め込む
mini-cms/admin/index.php
<tbody>
  <?php foreach($result as $row) : ?>
  <tr>
    <td><?php echo h($row['id']); ?></td>
    <td><?php echo h($row['title']); ?></td>
    <td><time datetime="<?php echo h($row['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['created']))); ?></time></td>
    <td><time datetime="<?php echo h($row['modified']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['modified']))); ?></time></td>
    <td>
      <form action="delete.php" method="post">
        <input type="hidden" name="id" value="<?php echo h($row['id']); ?>">
        <input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>">
        <input type="submit" value="削除">
      </form>
    </td>
  </tr>
  <?php endforeach; ?>
</tbody>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. トークンが埋め込まれているかを確認
  4. 「mini-cms」 › 「admin」フォルダ内にある「delete.php」をテキストエディタで開く
  5. セッションを開始後、 作成した独自関数を利用してトークンのチェックを行う。
mini-cms/admin/delete.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // POSTかダイレクトかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
    // POSTじゃない場合
    header('Location: index.php');
    exit();
  }

  // CSFR対策 ・・・ トークンを確認
  check_token();

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. きちんと削除できるかをチェック
ブラウザでの表示例

RERUN

練習問題 今回の理解度をチェック。

完成例を参考に「tranig」フォルダ内の「index.php」と「delete.php」を使って
カテゴリの管理画面と、カテゴリ削除機能を作成して下さい。

完成イメージ

RERUN

解答例
chapter19/training/config.php
<?php
  // データベースの定数
  define('DB_NAME', 'mini_cms_app');
  define('DB_USER', 'root');
  define('DB_PASSWORD', ''); // パスワード (MAMPは「root」)
  define('DSN', 'mysql:host=localhost;dbname='. DB_NAME . ';charset=utf8');
chapter19/training/functions.php
<?php
// XSS 対策
function h($s) {
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// CSRF対策 トークンの生成
function set_token() {
  if (!isset($_SESSION['token'])) {
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
  }
}

// CSRF対策 トークンの確認
function check_token() {
  if (empty($_POST['token']) || $_POST['token'] != $_SESSION['token']) {
    echo '不正な投稿(トークンが一致しません。)';
    exit();
  }
}
chapter19/training/index.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');

  // CSRF対策 ・・・ トークンの生成
  set_token();

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'SELECT * FROM categories';

    // SQLを実行
    $stmt = $dbh->query($sql);

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // print_r($result);

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>カテゴリの管理</title>
</head>
<body>
  <h1>カテゴリの管理</h1>

  <table border="1">
    <thead>
      <tr>
        <th>ID</th>
        <th>カテゴリ名</th>
        <th>削除</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach($result as $row) : ?>
      <tr>
        <td><?php echo h($row['id']); ?></td>
        <td><?php echo h($row['category_name']); ?></td>
        <td>
          <form action="delete.php" method="post">
            <input type="hidden" name="id" value="<?php echo h($row['id']); ?>">
            <input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>">
            <input type="submit" value="削除">
          </form>
        </td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
</body>
</html>
chapter19/training/delete.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');

  // POSTかダイレクトかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
    // POSTじゃない場合
    header('Location: index.php');
    exit();
  }

  // CSFR対策 ・・・ トークンを確認
  check_token();

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'DELETE FROM categories WHERE id = ?';

    // ステートメント用意
    $stmt = $dbh->prepare($sql);

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$_POST['id'] , PDO::PARAM_INT);

    // ステートメントを実行
    $stmt->execute();

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>カテゴリの削除</title>
</head>
<body>
  <h1>カテゴリの削除</h1>
  <p>カテゴリを削除しました。</p>

  <p><a href="./">一覧に戻る</a></p>
</body>
</html>

解答例は全問題のチェックボックスが on になるとご覧いただけます。

まとめ 削除はDELETE文。

削除機能は記事のIDを隠しフィールドに埋め込んで、DELETE文で削除する。

  • 削除をするにはDELETE
  • 削除機能はPOSTはで記事IDを飛ばす