Chapter21 ページング機能

概要と目標 ページング機能を実装し、
1ページあたりの表示件数を制限しよう。

一覧ページは、記事数が多くなるとスクロール量が多くなるため、
1ページあたりの表示件数を制限するページング機能を実装しましょう。

今回のゴール

RERUN

ページングの処理内容 SELECT文に、
LIMITを付ける。

ページング機能は、SELECT文のLIMITで表示件数を制限する。
今回は、1ページあたりの表示件数を「5」件にしたい。
そのため、1ページ目は「1件目」から「5件」表示し、
2ページ目は「6件目」から「5件」表示・・・としたい。
この記事を取得する開始位置を計算する必要がある。

記事を取得する開始位置を
求める計算式を考えてみよう。

LIMITの開始位置は、「0」から始まる。
従って、1件目は「0」、6件目は「5」となる。
この、ページごとの開始位置を計算式で求めたい。

ページごとの開始位置を求める計算式
(現在のページ - 1) × 1ページあたりの表示件数

答えを見る


ページング機能が有効になるように、
記事を登録しよう。

  1. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  2. 記事が「11」件以上登録されているかを確認
  3. 記事が「10」件以下の場合は、
    ブラウザで「mini-cms」 › 「admin」フォルダ内の「post.php」にアクセスし記事を登録
    http://localhost/php-lessons/mini-cms/admin/post.php

ページ番号と表示件数 GETパラメータで、
現在のページを管理。

ページングのページ番号はGETパラメータで管理する。

現在のページと1ページあたりの表示件数を
変数で管理しよう。

  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();

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page'])) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

  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();
  }
?>
  1. 上書き保存

記事取得数の制限 取得する開始位置を計算し、
LIMITで制限する。

LIMITの開始位置を計算するために必要なものが揃ったので、。
開始位置を計算し、SELECT文に LIMITを付ける

記事を取得する
開始位置を計算してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. 「(現在のページ番号 - 1) × 1ページあたりの表示数」でレコードの取得開始位置を計算する
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

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

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

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

    // 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();
  }
?>

LIMIT句を使って、
記事の抽出数を制限してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. 記事を抽出しているSELECT文をLIMIT句で制限する
    (変数を使うためプリペアド・ステートメントに変更)
    せっかくなので、レコードの並び順も新着順にしておく
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

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

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

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

    // SQL文の作成
    $sql = 'SELECT * FROM posts ORDER BY created DESC LIMIT ?, ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$start , PDO::PARAM_INT);
    $stmt->bindValue(2, (int)$limit , PDO::PARAM_INT);

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

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(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. URLに手動でGETパラメータを付加(「?page=2」など)し、
    ページに表示される記事が正しく取得できていることを確認
ブラウザでの表示例

RERUN

ただし、GETパラメータに存在しないページ番号を入力しても移動してしまう。
$page 変数が存在するページ以外にならない対策が必要。

存在するページ
もし全レコード数が「13」件だった場合、存在するページは、「1」~「3」ページになる。
従って最大のページ数は「3」となるので「4」ページ以降や、「1」ページ未満は存在しない。
最大ページ数を求める計算式
全レコード数 ÷ 1ページあたりの表示件数
(余りは繰り上げ)

答えを見る

全記事数の取得 最大ページ数を割り出すために、
全記事数を取得する。

最大ページ数を計算するには、記事が全部で何件あるかを取得し、
それを、1ページあたりの表示件数で割る必要がある。
従ってまず、データベースから集約関数を使って、全レコード数を取得する。

全記事数を取得してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. SELECT文で、「posts」テーブルの全記事数を取得する
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

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

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

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // SQL文の作成 レコード数を抽出
    $sql = 'SELECT count(*) AS total FROM posts';

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

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

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

    // SQL文の作成
    $sql = 'SELECT * FROM posts ORDER BY created DESC LIMIT ?, ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$start , PDO::PARAM_INT);
    $stmt->bindValue(2, (int)$limit , PDO::PARAM_INT);

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

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(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ページあたりの表示数 。

ページングに必要なページ数を割り出すには、
トータル記事数 ÷ 1ページあたりの表示数の端数を繰り上げする。

ceil関数 ・・・ 繰り上げをする関数
ceil(繰り上げする数値)

詳細はPHPマニュアルを参照

トータルページ数を計算してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. print_r($count);をコメントにし、
    「トータル記事数 ÷ 1ページあたりの表示数」の端数を繰り上げて、必要なページ数を計算
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

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

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

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // SQL文の作成 レコード数を抽出
    $sql = 'SELECT count(*) AS total FROM posts';

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

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

    // トータルページ数
    $total = ceil($count['total'] / $limit);

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

    // SQL文の作成
    $sql = 'SELECT * FROM posts ORDER BY created DESC LIMIT ?, ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$start , PDO::PARAM_INT);
    $stmt->bindValue(2, (int)$limit , PDO::PARAM_INT);

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

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

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

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

存在しないページの対策 存在しなページ番号が指定されたら、
存在する範囲に修正する。

URLのGETパラメータに、存在しなページ番号が直接入力された場合、
「1」 ~ トータルページ数の範囲内に修正する。

max関数 ・・・ 指定された数値の中から最大値を取得する関数
max(数値1, 数値2)

引数: 引数は「,(半角カンマ)」で複数指定できる

詳細はPHPマニュアルを参照

min関数 ・・・ 指定された数値の中から最小値を取得する関数
min(数値1, 数値2)

引数: 引数は「,(半角カンマ)」で複数指定できる

詳細はPHPマニュアルを参照

存在しないページの対策をしてみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. max関数を使い、ページ番号が「1」より小さくならない対策を行い、
    min関数を使い、ページ番号がトータルページ数より大きくならない対策を行う
mini-cms/admin/index.php
<?php
  // セッションの開始
  session_start();

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

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

  // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // SQL文の作成 レコード数を抽出
    $sql = 'SELECT count(*) AS total FROM posts';

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

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

    // トータルページ数
    $total = ceil($count['total'] / $limit);

    // $page が存在しないページ番号にならない対策
    $page = max($page, 1);  // 1より小さくならない
    $page = min($page, $total);  // トータルページ数より大きくならない

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

    // SQL文の作成
    $sql = 'SELECT * FROM posts ORDER BY created DESC LIMIT ?, ?';

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$start , PDO::PARAM_INT);
    $stmt->bindValue(2, (int)$limit , PDO::PARAM_INT);

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

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(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. URLに手動で存在しないGETパラメータを付加(「?page=0」など)し、
    ページに移動しないことを確認
ブラウザでの表示例

RERUN

リンクの作成 前のページがあれば表示。
次のページがあれば表示。

ページングのリンクは、リンク先にページ数のGETパラメータを付加して、
次のページ、または前のページを指定する。

ページングのリンクを作成してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内の「index.php」をテキストエディタで開く
  2. 現在のページより前のページがあれば「前のページへ」のリンクを表示し、
    現在のページより次のページがあれば「次のページへ」のリンクを表示
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="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>">
          <input type="submit" value="削除">
        </form>
      </td>
    </tr>
    <?php endforeach; ?>
  </tbody>
</table>

<nav>
  <h2>ページナビゲーション</h2>
  <ul>
    <?php if ( $page > 1 ) : ?>
    <li><a href="index.php?page=<?php echo h( $page - 1 ); ?>">前のページへ</a></li>
    <?php endif; ?>
    <?php if ( $page < $total ) : ?>
    <li><a href="index.php?page=<?php echo h( $page + 1 ); ?>">次のページへ</a></li>
    <?php endif; ?>
  </ul>
</nav>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/
  3. 必要な時にリンクが表示されることを確認
ブラウザでの表示例

RERUN

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

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

完成イメージ

RERUN

    • 1ページあたりの表示件数: 5件
解答例
chapter21/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');
chapter21/training/functions.php
<?php
  // XSS 対策
  function h($s) {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
  }
chapter21/training/index.php
<?php
  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');

   // 現在のページを取得
  $page = 1; // 初期値
  if ( isset($_GET['page']) && !empty($_GET['page']) ) {
    $page = $_GET['page'];
  }

  // 1ページ辺りの表示件数
  $limit = 5;

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

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

    // SQL文の作成 レコード数を抽出
    $sql = 'SELECT count(*) AS total FROM categories';

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

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

    // トータルページ数
    $total = ceil($count['total'] / $limit);

    // $page が存在しないページ番号にならない対策
    $page = max($page, 1);  // 1より小さくならない
    $page = min($page, $total);  // トータルページ数より大きくならない

    // 取得する投稿の開始位置
    $start = ($page - 1) * $limit;

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

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

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, (int)$start , PDO::PARAM_INT);
    $stmt->bindValue(2, (int)$limit , PDO::PARAM_INT);

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

    // 実行結果を連想配列として取得
    $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>
      </tr>
    </thead>
    <tbody>
      <?php foreach($result as $row) : ?>
      <tr>
        <td><?php echo h($row['id']); ?></td>
        <td><?php echo h($row['category_name']); ?></td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>

  <nav>
    <h2>ページナビゲーション</h2>
    <ul>
      <?php if ( $page > 1 ) : ?>
      <li><a href="index.php?page=<?php echo h( $page - 1 ); ?>">前のページへ</a></li>
      <?php endif; ?>
      <?php if ( $page < $total ) : ?>
      <li><a href="index.php?page=<?php echo h( $page + 1 ); ?>">次のページへ</a></li>
      <?php endif; ?>
    </ul>
  </nav>
</body>
</html>

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

まとめ 記事一覧が多いの場合は、
ページングでページを分ける。

記事数が多くなると記事一覧ページが、縦にとても長くなる。
ページングを活用することで、複数ページにわけることが出来る。

  • ページ番号はGETパラメータで管理
  • トータルページ数や投稿の開始位置は計算する
  • 存在しないページ表示しないようにする