Chapter20 記事の編集機能

概要と目標 一度投稿した内容を、
変更できる機能を作ろう。

投稿内容を変更できる機能を作成しましょう。

今回のゴール

RERUN

編集ボタンの追加 記事のIDをパラメータに付けた
編集ページへのリンクを追加する。

テーブルの列に「編集」ボタンを追加する。
その際、リンク先URLに記事のIDをパラメータで付加しておく。

編集ボタンの追加してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. 各レコードの「削除」の手前の列に「編集」と文字を追加し、
    記事のIDをパラメータ付加した「edit.php」へのリンクを設定
mini-cms/admin/index.php
<table border="1">
  <thead>
    <tr>
      <th>ID</th>
      <th>タイトル</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><a href="edit.php?id=<?php echo h($row['id']); ?>">編集</a></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/
ブラウザでの表示例

RERUN

POSTかGETか
どちらを使っても同じような処理を作れるが、GETは、Googleのクローラーなどもアクセスする可能性がある。
その際にレコードが変更されることは予期しないので、 データベースのレコードを書き換えたりするような処理を行うときはPOSTを、 レコードを引っ張って来るときはGETを使うことが多い。

編集用フォームの作成 記事のIDと一致するレコードを取得し、
各コントール部品の初期値として指定する。

編集用フォームは、まず受け取った記事のIDと一致するレコードをデータベースから取得する。
そして、それらの値を、フォームの各コントール部品の初期値として割り当てればよい。

記事IDと一致するレコードを取得しよう

  1. 「mini-cms」 › 「admin」フォルダ内に「edit.php」を作成
  2. $_GET['id']に記事IDが付加されていれば、
    データベースから、記事のIDと一致するレコードを取得
    記事IDがなければ、「index.php」にリダイレクトする
    (「post.php」からコピーできるところは、コピーする)
mini-cms/admin/edit.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // GETパラメータのチェック
  if ( empty($_GET['id']) ) {
    // $_GET['id'] が 空 の場合
    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 = 'SELECT * FROM posts WHERE id = ?';

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

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

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

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

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

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 「編集」のリンクをクリックし、レコードが取得出来ているかを確認
ブラウザでの表示例

RERUN


編集用フォームを作成しよう

  1. データベースから、全てのカテゴリを取得し、
    HTMLの雛形と「update.php」に送信するform要素、各コントロール部品を作成
    (「post.php」からコピーできるところは、コピーする)
mini-cms/admin/edit.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // GETパラメータのチェック
  if ( empty($_GET['id']) ) {
    // $_GET['id'] が 空 の場合
    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 = 'SELECT * FROM posts WHERE id = ?';

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

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

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

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

    // SQL文の作成 カテゴリの全レコードを抽出
    $sql = 'SELECT * FROM categories';

    // クエリの実行
    $stmt = $dbh->query($sql);

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

    // データベースとの接続を終了
    $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>
  <form action="update.php" method="post">
    <dl>
      <dt><label for="title">記事のタイトル</label></dt>
      <dd>
        <input type="text" id="title" name="title">
      </dd>
      <dt><label for="category_id">カテゴリー</label></dt>
      <dd>
        <select name="category_id" id="category_id">
          <?php foreach($categories as $category) : ?>
          <option value="<?php echo h($category['id']); ?>"><?php echo h($category['category_name']); ?></option>
          <?php endforeach; ?>
        </select>
      </dd>
      <dt><label for="content">記事の内容</label></dt>
      <dd>
        <textarea name="content" id="content" cols="30" rows="10"></textarea>
      </dd>
    </dl>
    <p><input type="submit" value="変更"></p>
  </form>
</body>
</html>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 「編集」のリンクをクリックし、フォームが出来ているかを確認
ブラウザでの表示例

RERUN


初期値を設定してみよう。

  1. print_r($result);の部分をコメントにし、
    データベースから取得した値を 各コントロール部品の初期値に指定する
    なお、記事IDは、<input type="hidden">で指定する
mini-cms/admin/edit.php
// print_r($result);
mini-cms/admin/edit.php
<form action="update.php" method="post">
  <dl>
    <dt><label for="title">記事のタイトル</label></dt>
    <dd>
      <input type="text" id="title" name="title" <?php echo h($result['title']); ?>>
    </dd>
    <dt><label for="category_id">カテゴリー</label></dt>
    <dd>
      <select name="category_id" id="category_id">
        <?php foreach($categories as $category) : ?>
        <?php
          $selected = '';
          // ループで出力するカテゴリと、データベースから取得したカテゴリが一致するかチェック
          if ($category['id'] == $result['category_id']) {
            // 一致した場合
            $selected = ' selected';
          }
        ?>
        <option value="<?php echo h($category['id']); ?>"<?php echo h($selected); ?>><?php echo h($category['category_name']); ?></option>
        <?php endforeach; ?>
      </select>
    </dd>
    <dt><label for="content">記事の内容</label></dt>
    <dd>
      <textarea name="content" id="content" cols="30" rows="10"><?php echo h($result['content']); ?></textarea>
    </dd>
  </dl>
  <p><input type="hidden" name="id" value="<?php echo h($result['id']); ?>"></p>
  <p><input type="submit" value="変更"></p>
</form>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 「編集」のリンクをクリックし、フォームに初期値が設定されているかを確認
ブラウザでの表示例

RERUN

編集処理 編集用のフォームから受け取った値を
UPDATE文で更新する。

編集処理はGETパラメータに付加された記事IDと一致するメッセージを
UPDATE文で更新する。

編集処理を作成してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内に「update.php」を作成
  2. POST送信されてなかったら管理画面にリダイレクトし、
    POST送信されている場合は、記事IDを使って記事を更新し、その旨のメッセージを表示
mini-cms/admin/update.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // 変更ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== '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 = 'UPDATE posts SET title=?, category_id=?, content=? WHERE id = ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, $_POST['title'] , PDO::PARAM_STR);
    $stmt->bindValue(2, (int)$_POST['category_id'] , PDO::PARAM_INT);
    $stmt->bindValue(3, $_POST['content'] , PDO::PARAM_STR);
    $stmt->bindValue(4, (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対策が必要。

現在の仕様では、外部フォームから「update.php」にデータを送信されても更新する
このままでは、CSRFの脆弱性があるので対策する。

CSRF対策をしよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「edit.php」をテキストエディタで開く
  2. SESSIONを開始し、独自関数を使ってトークンを生成
    生成したトークンは<input type="hidden">でフォームに埋め込む
mini-cms/admin/edit.php
<?php
  // セッションの開始
  session_start();

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

  // GETパラメータのチェック
  if ( empty($_GET['id']) ) {
    // $_GET['id'] が 空 の場合
    header('Location: index.php');
    exit();
  }

  // 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 posts WHERE id = ?';

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

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

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

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

    // SQL文の作成 カテゴリの全レコードを抽出
    $sql = 'SELECT * FROM categories';

    // クエリの実行
    $stmt = $dbh->query($sql);

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

    // データベースとの接続を終了
    $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>
  <form action="update.php" method="post">
    <dl>
      <dt><label for="title">記事のタイトル</label></dt>
      <dd>
        <input type="text" id="title" name="title" value="<?php echo h($result['title']); ?>">
      </dd>
      <dt><label for="category_id">カテゴリー</label></dt>
      <dd>
        <select name="category_id" id="category_id">
          <?php foreach($categories as $category) : ?>
          <?php
            $selected = '';
            // ループで出力するカテゴリと、データベースから取得したカテゴリが一致するかチェック
            if ($category['id'] == $result['category_id']) {
              // 一致した場合
              $selected = ' selected';
            }
          ?>
          <option value="<?php echo h($category['id']); ?>"<?php echo h($selected); ?>><?php echo h($category['category_name']); ?></option>
          <?php endforeach; ?>
        </select>
      </dd>
      <dt><label for="content">記事の内容</label></dt>
      <dd>
        <textarea name="content" id="content" cols="30" rows="10"><?php echo h($result['content']); ?></textarea>
      </dd>
    </dl>
    <p><input type="hidden" name="id" value="<?php echo h($result['id']); ?>"></p>
    <p><input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>"></p>
    <p><input type="submit" value="変更"></p>
  </form>
</body>
</html>

答えを見る

  1. 上書き保存
  2. 「mini-cms」 › 「admin」フォルダ内の「update.php」をテキストエディタで開く
  3. セッションを開始した後、独自関数を使って、
    フォームに埋め込んだトークンとSESSIONに格納したトークンが一致しているかを確認
mini-cms/admin/update.php
<?php
  // セッションの開始
  session_start();

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

  // 変更ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: index.php');
    exit();
  }

 // CSRF対策 ・・・ トークンのチェック
  check_token();

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

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

    // SQL文の作成
    $sql = 'UPDATE posts SET title=?, category_id=?, content=? WHERE id = ?';

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

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

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

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

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 「編集」をクリックし、トークンが埋め込まれていることを確認
ブラウザでの表示例

RERUN

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

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

完成イメージ

RERUN

解答例
chapter20/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');
chapter20/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();
    }
  }
chapter20/training/index.php
<?php
  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');

  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><a href="edit.php?id=<?php echo h($row['id']); ?>">編集</a></td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
</body>
</html>
chapter20/training/edit.php
<?php
  // セッションの開始
  session_start();

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

  // GETパラメータのチェック
  if ( empty($_GET['id']) ) {
    // $_GET['id'] が 空 の場合
    header('Location: index.php');
    exit();
  }

  // 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 WHERE id = ?';

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

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

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

    // 実行結果を連想配列として取得
    $result = $stmt->fetch(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>
  <form action="update.php" method="post">
    <dl>
      <dt><label for="category_name">カテゴリ名</label></dt>
      <dd>
        <input type="text" id="category_name" name="category_name" value="<?php echo h($result['category_name']); ?>">
      </dd>
    </dl>
    <p><input type="hidden" name="id" value="<?php echo h($result['id']); ?>"></p>
    <p><input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>"></p>
    <p><input type="submit" value="変更"></p>
  </form>
</body>
</html>
chapter20/training/update.php
<?php
  // セッションの開始
  session_start();

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

  // 変更ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: index.php');
    exit();
  }

 // CSRF対策 ・・・ トークンのチェック
  check_token();

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

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

    // SQL文の作成
    $sql = 'UPDATE categories SET category_name=? WHERE id = ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, $_POST['category_name'] , PDO::PARAM_STR);
    $stmt->bindValue(2, (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 になるとご覧いただけます。

まとめ 編集するIDと一致するレコードを取得し、
UPDATE文でレコードを更新する。

編集機能は記事のIDをパラメータに付加し、そのIDと一致するレコードを取得。
取得したレコードをフォームの初期値に割り当てて、UPDATE文で更新する。

  • コントロール部品には初期値を指定できる
  • 更新をするにはUPDATE文を使う
  • 送信フォームにはCSRF対策を行う