PHPで検索フォームを作る方法について

採用はこちら

PHPで検索フォームを作るには、まずHTMLでフォームを用意し、入力されたキーワードをPHPで受け取り、データベースや配列などの検索対象から一致するデータを取得して表示します。

基本的な流れは、次の通りです。

  1. HTMLで検索フォームを作る
  2. PHPで検索キーワードを受け取る
  3. 入力値を整える
  4. データベースや配列から検索する
  5. 検索結果を画面に表示する

検索フォーム自体はシンプルに作れますが、実務では SQLインジェクション対策XSS対策 も必要です。

この記事では、PHPで検索フォームを作る基本から、MySQLを使った検索、複数キーワード検索、ページネーション、WordPressでの検索フォームまで詳しく解説します。

目次

PHP検索フォームの基本

検索フォームの基本構造

まずは、HTMLで検索フォームを作ります。

<form action="search.php" method="get">
  <input type="search" name="keyword" placeholder="キーワードを入力">
  <button type="submit">検索</button>
</form>

このフォームでは、入力されたキーワードが keyword という名前で search.php に送信されます。

例えば、ユーザーが「PHP」と入力して検索すると、URLは次のようになります。

search.php?keyword=PHP

PHP側では、この keyword を受け取って検索処理を行います。

検索フォームではGETを使うのが一般的

検索フォームでは、基本的に GET メソッドを使います。

<form action="search.php" method="get">

GET を使うと、検索条件がURLに表示されます。

例えば、以下のようなURLになります。

search.php?keyword=WordPress

検索フォームで GET がよく使われる理由は、検索結果ページをブックマークしたり、URLを共有したりできるからです。

一方、ログインフォームや問い合わせフォームのように、送信内容をURLに表示したくない場合は POST を使います。

メソッド特徴主な用途
GETURLにパラメータが表示される検索、絞り込み、ページネーション
POSTURLにパラメータが表示されないログイン、会員登録、問い合わせ

通常のサイト内検索や記事検索であれば、GET を使うのが自然です。

配列を使った簡単な検索フォーム

データベースなしで検索フォームを作る

まずは、MySQLなどのデータベースを使わず、PHPの配列から検索する簡単な例を見てみましょう。

<?php
$items = [
    'PHP入門',
    'JavaScript基礎',
    'WordPressテーマ制作',
    'HTMLとCSSの基本',
    'Laravel入門',
    'MySQLデータベース講座'
];

$keyword = $_GET['keyword'] ?? '';

if (!is_string($keyword)) {
    $keyword = '';
}

$keyword = trim($keyword);

$results = [];

if ($keyword !== '') {
    foreach ($items as $item) {
        if (mb_stripos($item, $keyword, 0, 'UTF-8') !== false) {
            $results[] = $item;
        }
    }
}

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>検索フォーム</title>
</head>
<body>

<h1>検索フォーム</h1>

<form action="" method="get">
  <label for="keyword">検索キーワード</label>
  <input
    id="keyword"
    type="search"
    name="keyword"
    value="<?php echo h($keyword); ?>"
    placeholder="キーワードを入力"
  >
  <button type="submit">検索</button>
</form>

<hr>

<?php if ($keyword === ''): ?>

  <p>キーワードを入力してください。</p>

<?php else: ?>

  <h2>「<?php echo h($keyword); ?>」の検索結果</h2>

  <?php if (count($results) > 0): ?>
    <ul>
      <?php foreach ($results as $result): ?>
        <li><?php echo h($result); ?></li>
      <?php endforeach; ?>
    </ul>
  <?php else: ?>
    <p>検索結果が見つかりませんでした。</p>
  <?php endif; ?>

<?php endif; ?>

</body>
</html>

$_GETで検索キーワードを受け取る

検索キーワードは、PHPの $_GET で受け取ります。

$keyword = $_GET['keyword'] ?? '';

?? は、値が存在しない場合に空文字を入れるための書き方です。

例えば、URLに keyword がない場合でもエラーにならず、空文字として扱えます。

$keyword = $_GET['keyword'] ?? '';

ただし、悪意あるアクセスによって keyword[]=test のように配列が送信される場合もあります。

そのため、より安全にするなら次のように文字列かどうかを確認します。

if (!is_string($keyword)) {
    $keyword = '';
}

その後、trim() で前後の余分な空白を取り除きます。

$keyword = trim($keyword);

mb_stripos()で部分一致検索をする

配列の中からキーワードを探す場合は、mb_stripos() を使うと便利です。

if (mb_stripos($item, $keyword, 0, 'UTF-8') !== false) {
    $results[] = $item;
}

mb_stripos() は、文字列の中に指定したキーワードが含まれているかを調べる関数です。

例えば、検索キーワードが「PHP」の場合、次のようなデータに一致します。

PHP入門

また、mb_stripos() は大文字・小文字を区別しません。

php
PHP
Php

このような表記の違いがあっても検索できます。

日本語では英語ほど大文字・小文字の違いを意識する場面は多くありませんが、英数字を含む検索では便利です。

htmlspecialchars()でXSS対策をする

検索フォームで入力された値を画面に表示するときは、必ずエスケープします。

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

画面に出力するときは、以下のように使います。

echo h($keyword);

これはXSS対策のためです。

例えば、ユーザーが以下のような文字列を入力したとします。

<script>alert('test')</script>

この値をそのままHTMLに出力すると、JavaScriptが実行される危険があります。

そのため、HTMLに表示する値は htmlspecialchars() でエスケープする必要があります。

検索フォームでは、ユーザーが入力したキーワードだけでなく、データベースから取得したタイトルや本文もエスケープして表示するのが基本です。

MySQLを使ったPHP検索フォーム

MySQL検索フォームの仕組み

実際のWebサイトでは、検索対象のデータをMySQLなどのデータベースに保存しているケースが多いです。

PHPでMySQL検索フォームを作る場合は、次のような流れになります。

  1. 検索フォームからキーワードを送信する
  2. PHPでキーワードを受け取る
  3. PDOでMySQLに接続する
  4. SQLの LIKE を使って検索する
  5. 検索結果をHTMLに表示する

ここでは、記事データを検索する例で解説します。

サンプルのテーブル構成

以下のような articles テーブルがあるとします。

CREATE TABLE articles (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  body TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

サンプルデータは次の通りです。

INSERT INTO articles (title, body) VALUES
('PHP入門', 'PHPの基本文法について解説します。'),
('WordPressテーマ制作', 'WordPressテーマを自作する方法を紹介します。'),
('MySQLの基本', 'MySQLでデータを管理する方法を解説します。'),
('JavaScript入門', 'JavaScriptの基礎を学びます。'),
('Laravel入門', 'Laravelを使ったWebアプリ開発を紹介します。');

このテーブルの titlebody を対象に検索します。

PDOでデータベースに接続する

PHPからMySQLに接続する場合は、PDOを使うのが一般的です。

以下のように db.php を作成します。

<?php
$host = 'localhost';
$dbname = 'sample_db';
$user = 'root';
$password = '';

try {
    $pdo = new PDO(
        "mysql:host={$host};dbname={$dbname};charset=utf8mb4",
        $user,
        $password,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );
} catch (PDOException $e) {
    error_log($e->getMessage());
    exit('データベース接続に失敗しました。');
}

ポイントは、以下の3つです。

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

SQLエラーが起きたときに例外として扱えます。

PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC

取得結果を連想配列として扱えます。

PDO::ATTR_EMULATE_PREPARES => false

可能な限りMySQL側のネイティブプリペアドステートメントを使う指定です。

また、文字コードには utf8mb4 を指定しています。

charset=utf8mb4

日本語だけでなく、絵文字なども扱いやすくなります。

MySQL検索フォームの完成例

基本的な検索フォームのコード

以下は、MySQLの articles テーブルから記事を検索するサンプルです。

<?php
require_once 'db.php';

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function escapeLike($value) {
    return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
}

$keyword = $_GET['keyword'] ?? '';

if (!is_string($keyword)) {
    $keyword = '';
}

$keyword = trim($keyword);

$error = '';
$articles = [];

if (mb_strlen($keyword, 'UTF-8') > 100) {
    $error = '検索キーワードは100文字以内で入力してください。';
} elseif ($keyword !== '') {
    $likeKeyword = '%' . escapeLike($keyword) . '%';

    $stmt = $pdo->prepare(
        "SELECT id, title, body, created_at
         FROM articles
         WHERE title LIKE :keyword ESCAPE '\\\\'
            OR body LIKE :keyword ESCAPE '\\\\'
         ORDER BY created_at DESC"
    );

    $stmt->bindValue(':keyword', $likeKeyword, PDO::PARAM_STR);
    $stmt->execute();

    $articles = $stmt->fetchAll();
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>記事検索</title>
</head>
<body>

<h1>記事検索</h1>

<form action="" method="get">
  <label for="keyword">検索キーワード</label>
  <input
    id="keyword"
    type="search"
    name="keyword"
    value="<?php echo h($keyword); ?>"
    placeholder="キーワードを入力"
  >
  <button type="submit">検索</button>
</form>

<hr>

<?php if ($error !== ''): ?>

  <p><?php echo h($error); ?></p>

<?php elseif ($keyword === ''): ?>

  <p>検索キーワードを入力してください。</p>

<?php else: ?>

  <h2>「<?php echo h($keyword); ?>」の検索結果</h2>

  <?php if (count($articles) > 0): ?>

    <?php foreach ($articles as $article): ?>
      <article>
        <h3><?php echo h($article['title']); ?></h3>
        <p><?php echo nl2br(h($article['body'])); ?></p>
        <small><?php echo h($article['created_at']); ?></small>
      </article>
      <hr>
    <?php endforeach; ?>

  <?php else: ?>

    <p>検索結果が見つかりませんでした。</p>

  <?php endif; ?>

<?php endif; ?>

</body>
</html>

LIKEを使って部分一致検索をする

MySQLで部分一致検索をする場合は、LIKE を使います。

WHERE title LIKE :keyword

検索キーワードの前後に % を付けることで、文字列の一部に一致するデータを探せます。

$likeKeyword = '%' . $keyword . '%';

例えば、キーワードが「PHP」の場合、SQLとしては次のようなイメージになります。

WHERE title LIKE '%PHP%'

LIKE で使う % には、任意の0文字以上という意味があります。

書き方意味
PHP%PHPで始まる
%PHPPHPで終わる
%PHP%PHPを含む

検索フォームでは、一般的に %キーワード% のような部分一致検索がよく使われます。

%_をエスケープする理由

SQLの LIKE では、 %_ は特別な意味を持ちます。

文字意味
%任意の0文字以上
_任意の1文字

そのため、ユーザーが %_ を入力した場合、意図しない検索結果になることがあります。

例えば、ユーザーが % と検索すると、多くのデータに一致してしまう可能性があります。

そのような挙動を避けたい場合は、次のようにエスケープします。

function escapeLike($value) {
    return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
}

そして、SQL側では ESCAPE を指定します。

$stmt = $pdo->prepare(
    "SELECT id, title, body, created_at
     FROM articles
     WHERE title LIKE :keyword ESCAPE '\\\\'
        OR body LIKE :keyword ESCAPE '\\\\'
     ORDER BY created_at DESC"
);

通常の簡単な検索フォームであれば省略されることもありますが、実務向けに作るなら入れておくと安心です。

プリペアドステートメントでSQLインジェクション対策をする

検索フォームでは、ユーザーが入力した値をSQLに使います。

そのため、SQLインジェクション対策が必要です。

悪い例は以下です。

$sql = "SELECT * FROM articles WHERE title LIKE '%" . $_GET['keyword'] . "%'";

このように、ユーザー入力をSQLに直接連結してはいけません。

安全に書く場合は、PDOのプリペアドステートメントを使います。

$stmt = $pdo->prepare(
    "SELECT id, title, body, created_at
     FROM articles
     WHERE title LIKE :keyword
        OR body LIKE :keyword
     ORDER BY created_at DESC"
);

$stmt->bindValue(':keyword', '%' . $keyword . '%', PDO::PARAM_STR);
$stmt->execute();

prepare() でSQLを準備し、bindValue() で値を渡します。

これにより、ユーザーが入力した値をSQL文としてではなく、単なる値として扱えます。

検索フォームを作る場合は、最低限以下の2つを意識してください。

// SQLに渡すとき
prepare() と bindValue() を使う

// HTMLに表示するとき
htmlspecialchars() を使う

この2つは、検索フォームの基本的なセキュリティ対策です。

複数キーワード検索を作る方法

複数キーワード検索とは

検索フォームでは、1つのキーワードだけでなく、複数のキーワードで検索したい場合があります。

例えば、ユーザーが次のように入力した場合です。

PHP 入門

この場合、「PHP」と「入門」の両方を含む記事を検索することができます。

複数キーワード検索には、大きく分けて2種類あります。

検索方式意味
AND検索すべてのキーワードを含む
OR検索いずれかのキーワードを含む

一般的なサイト内検索では、検索結果を絞り込みやすいAND検索が使われることが多いです。

AND検索のコード例

以下は、複数キーワードをAND検索する例です。

<?php
require_once 'db.php';

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function escapeLike($value) {
    return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
}

$keyword = $_GET['keyword'] ?? '';

if (!is_string($keyword)) {
    $keyword = '';
}

$keyword = trim($keyword);

$articles = [];
$error = '';

if (mb_strlen($keyword, 'UTF-8') > 100) {
    $error = '検索キーワードは100文字以内で入力してください。';
} elseif ($keyword !== '') {
    $normalizedKeyword = str_replace(' ', ' ', $keyword);

    $keywords = preg_split('/\s+/', $normalizedKeyword, -1, PREG_SPLIT_NO_EMPTY);

    $keywords = array_slice($keywords, 0, 5);

    $where = [];
    $params = [];

    foreach ($keywords as $index => $word) {
        $titleParam = ':title_keyword' . $index;
        $bodyParam = ':body_keyword' . $index;

        $where[] = "(title LIKE {$titleParam} ESCAPE '\\\\' OR body LIKE {$bodyParam} ESCAPE '\\\\')";

        $params[$titleParam] = '%' . escapeLike($word) . '%';
        $params[$bodyParam] = '%' . escapeLike($word) . '%';
    }

    $sql = 'SELECT id, title, body, created_at FROM articles';

    if (count($where) > 0) {
        $sql .= ' WHERE ' . implode(' AND ', $where);
    }

    $sql .= ' ORDER BY created_at DESC';

    $stmt = $pdo->prepare($sql);

    foreach ($params as $param => $value) {
        $stmt->bindValue($param, $value, PDO::PARAM_STR);
    }

    $stmt->execute();

    $articles = $stmt->fetchAll();
}
?>

複数キーワード検索のポイント

複数キーワード検索では、まず全角スペースを半角スペースに変換します。

$normalizedKeyword = str_replace(' ', ' ', $keyword);

日本語サイトでは、ユーザーが全角スペースでキーワードを区切ることもあるためです。

次に、スペースでキーワードを分割します。

$keywords = preg_split('/\s+/', $normalizedKeyword, -1, PREG_SPLIT_NO_EMPTY);

PREG_SPLIT_NO_EMPTY を指定すると、余計な空白によって空のキーワードが混ざるのを防げます。

さらに、キーワード数が多すぎるとSQLが長くなりすぎるため、上限を設けます。

$keywords = array_slice($keywords, 0, 5);

この例では、最大5語までに制限しています。

OR検索に変更する方法

AND検索では、すべてのキーワードを含む記事だけが表示されます。

$sql .= ' WHERE ' . implode(' AND ', $where);

OR検索にしたい場合は、ANDOR に変更します。

$sql .= ' WHERE ' . implode(' OR ', $where);

例えば、検索キーワードが「PHP 入門」の場合、AND検索とOR検索では結果が変わります。

検索方式検索条件
AND検索PHPと入門の両方を含む
OR検索PHPまたは入門を含む

検索結果を絞り込みたいならAND検索、広く検索したいならOR検索が向いています。

カテゴリーで絞り込む検索フォーム

キーワードとカテゴリーを組み合わせる

検索フォームでは、キーワード検索に加えてカテゴリーで絞り込むケースもあります。

例えば、次のようなフォームです。

<form action="" method="get">
  <label for="keyword">検索キーワード</label>
  <input
    id="keyword"
    type="search"
    name="keyword"
    value="<?php echo h($keyword); ?>"
    placeholder="キーワードを入力"
  >

  <label for="category">カテゴリー</label>
  <select id="category" name="category">
    <option value="">すべてのカテゴリー</option>
    <option value="php">PHP</option>
    <option value="wordpress">WordPress</option>
    <option value="javascript">JavaScript</option>
  </select>

  <button type="submit">検索</button>
</form>

この場合、PHP側ではキーワード条件とカテゴリー条件を組み合わせます。

カテゴリー検索のコード例

テーブルに category カラムがある場合は、次のように書けます。

$keyword = $_GET['keyword'] ?? '';
$category = $_GET['category'] ?? '';

if (!is_string($keyword)) {
    $keyword = '';
}

if (!is_string($category)) {
    $category = '';
}

$keyword = trim($keyword);
$category = trim($category);

$allowedCategories = ['php', 'wordpress', 'javascript'];

$where = [];
$params = [];

if ($keyword !== '') {
    $where[] = "(title LIKE :keyword ESCAPE '\\\\' OR body LIKE :keyword ESCAPE '\\\\')";
    $params[':keyword'] = '%' . escapeLike($keyword) . '%';
}

if ($category !== '' && in_array($category, $allowedCategories, true)) {
    $where[] = 'category = :category';
    $params[':category'] = $category;
}

$sql = 'SELECT id, title, body, category, created_at FROM articles';

if (count($where) > 0) {
    $sql .= ' WHERE ' . implode(' AND ', $where);
}

$sql .= ' ORDER BY created_at DESC';

$stmt = $pdo->prepare($sql);

foreach ($params as $param => $value) {
    $stmt->bindValue($param, $value, PDO::PARAM_STR);
}

$stmt->execute();

$articles = $stmt->fetchAll();

カテゴリーの値は、ユーザーが自由に変更できるため、許可した値だけを使うようにします。

$allowedCategories = ['php', 'wordpress', 'javascript'];

このようにホワイトリスト化しておくと、存在しないカテゴリー値で検索されることを防ぎやすくなります。

ページネーション付き検索フォーム

検索結果が多い場合はページネーションを使う

検索結果が多い場合、すべてのデータを1ページに表示すると見づらくなります。

そのため、実務ではページネーションを使って、1ページあたりの表示件数を制限します。

例えば、1ページに10件ずつ表示する場合は、以下のように設定します。

$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;

if ($page < 1) {
    $page = 1;
}

$limit = 10;
$offset = ($page - 1) * $limit;

$limit は1ページあたりの表示件数です。

$offset は、何件目から取得するかを表します。

総件数を取得する

ページネーションを作るには、まず検索結果の総件数を取得します。

$countSql = "SELECT COUNT(*)
             FROM articles
             WHERE title LIKE :keyword ESCAPE '\\\\'
                OR body LIKE :keyword ESCAPE '\\\\'";

$countStmt = $pdo->prepare($countSql);
$countStmt->bindValue(':keyword', $likeKeyword, PDO::PARAM_STR);
$countStmt->execute();

$total = (int) $countStmt->fetchColumn();

総件数が分かれば、総ページ数を計算できます。

$totalPages = (int) ceil($total / $limit);

例えば、検索結果が35件で、1ページあたり10件表示する場合、総ページ数は4ページになります。

LIMITOFFSETで検索結果を取得する

ページごとの検索結果は、SQLの LIMITOFFSET を使って取得します。

$sql = "SELECT id, title, body, created_at
        FROM articles
        WHERE title LIKE :keyword ESCAPE '\\\\'
           OR body LIKE :keyword ESCAPE '\\\\'
        ORDER BY created_at DESC
        LIMIT :limit OFFSET :offset";

$stmt = $pdo->prepare($sql);

$stmt->bindValue(':keyword', $likeKeyword, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);

$stmt->execute();

$articles = $stmt->fetchAll();

LIMITOFFSET は整数として扱うため、PDO::PARAM_INT を指定します。

$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);

ページネーションリンクを作る

ページネーションリンクは、検索キーワードを維持したままページ番号だけを変える必要があります。

<?php if ($totalPages > 1): ?>
  <nav>
    <?php for ($i = 1; $i <= $totalPages; $i++): ?>
      <?php
      $query = [
          'keyword' => $keyword,
          'page' => $i,
      ];

      $url = '?' . http_build_query($query);
      ?>

      <?php if ($i === $page): ?>
        <strong><?php echo $i; ?></strong>
      <?php else: ?>
        <a href="<?php echo h($url); ?>"><?php echo $i; ?></a>
      <?php endif; ?>
    <?php endfor; ?>
  </nav>
<?php endif; ?>

http_build_query() を使うと、検索条件をURLに安全に組み立てやすくなります。

カテゴリーや並び替え条件がある場合も、配列に追加するだけで対応できます。

$query = [
    'keyword' => $keyword,
    'category' => $category,
    'page' => $i,
];

検索キーワードをハイライト表示する方法

ハイライト表示とは

検索結果の中で、検索キーワードに一致した部分を目立たせたい場合があります。

例えば、「PHP」で検索した場合に、検索結果のタイトルや本文中の「PHP」を以下のように表示します。

<mark>PHP</mark>入門

HTMLの <mark> タグを使うと、該当部分をハイライトできます。

ハイライト関数の例

単一キーワードをハイライトする簡単な関数は以下です。

function highlightKeyword($text, $keyword) {
    if ($keyword === '') {
        return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    $pattern = '/' . preg_quote($keyword, '/') . '/iu';

    $parts = preg_split(
        $pattern,
        $text,
        -1,
        PREG_SPLIT_DELIM_CAPTURE
    );

    if ($parts === false) {
        return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    $result = '';

    foreach ($parts as $part) {
        if (preg_match($pattern, $part)) {
            $result .= '<mark>' . htmlspecialchars($part, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</mark>';
        } else {
            $result .= htmlspecialchars($part, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        }
    }

    return $result;
}

使い方は以下です。

<h3><?php echo highlightKeyword($article['title'], $keyword); ?></h3>

ハイライト処理では、HTMLエスケープ後の文字列を直接置換するよりも、元の文字列を分割して、表示する直前に各部分をエスケープする方が安全です。

複数キーワードのハイライトには注意する

複数キーワードをハイライトする場合は、キーワード同士が重複する可能性があります。

例えば、次のようなキーワードです。

PHP PHP入門

このような場合、単純な置換処理ではHTMLが崩れたり、意図しない箇所がハイライトされたりすることがあります。

そのため、複数キーワードのハイライトを本格的に実装する場合は、キーワードの並び順や重複処理も考慮する必要があります。

初心者向けの検索フォームでは、まずは単一キーワードのハイライトから実装するとよいでしょう。

検索フォームの見た目を整えるCSS

HTMLの例

検索フォームは、HTMLだけでも作れますが、CSSを少し追加すると見やすくなります。

<form action="" method="get" class="search-form">
  <label for="keyword" class="search-label">検索キーワード</label>
  <input
    id="keyword"
    type="search"
    name="keyword"
    value="<?php echo h($keyword); ?>"
    placeholder="キーワードを入力してください"
    class="search-input"
  >
  <button type="submit" class="search-button">検索</button>
</form>

label を付けておくと、アクセシビリティの面でも良くなります。

CSSの例

簡単なCSSは以下です。

.search-form {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 640px;
}

.search-label {
  font-weight: bold;
}

.search-input {
  flex: 1;
  padding: 10px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.search-button {
  padding: 10px 20px;
  border: none;
  background: #333;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
}

.search-button:hover {
  opacity: 0.8;
}

スマートフォンでも使いやすくするなら、幅が狭い画面で縦並びにするとよいです。

@media (max-width: 600px) {
  .search-form {
    flex-direction: column;
    align-items: stretch;
  }

  .search-button {
    width: 100%;
  }
}

WordPressで検索フォームを作る方法

WordPressでは標準検索機能を使う

WordPressで検索フォームを作る場合は、PHPで一から検索処理を書くのではなく、WordPressの標準検索機能を使うのが基本です。

WordPressでは、検索キーワードのパラメータ名は s です。

<input type="search" name="s">

例えば、「PHP」と検索すると、URLは次のようになります。

https://example.com/?s=PHP

searchform.phpを作成する

テーマフォルダに searchform.php を作成すると、WordPress標準の検索フォームをカスタマイズできます。

<form role="search" method="get" class="search-form" action="<?php echo esc_url(home_url('/')); ?>">
  <label for="search-field">
    <span class="screen-reader-text">検索:</span>
  </label>

  <input
    id="search-field"
    type="search"
    class="search-field"
    placeholder="キーワードを入力"
    value="<?php echo esc_attr(get_search_query()); ?>"
    name="s"
  >

  <button type="submit" class="search-submit">検索</button>
</form>

ポイントは、name="s" を使うことです。

name="s"

また、検索キーワードをHTML属性に出力するため、esc_attr() を使います。

value="<?php echo esc_attr(get_search_query()); ?>"

検索フォームを表示する

テーマ内で検索フォームを表示したい場所に、以下を書きます。

<?php get_search_form(); ?>

これで、テーマ内の searchform.php が読み込まれます。

検索結果ページを作る

WordPressの検索結果ページは、通常 search.php というテンプレートファイルで作ります。

<?php get_header(); ?>

<main class="search-results-page">

  <h1>
    「<?php echo esc_html(get_search_query()); ?>」の検索結果
  </h1>

  <?php if (have_posts()): ?>

    <?php while (have_posts()): the_post(); ?>

      <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
        <h2>
          <a href="<?php echo esc_url(get_permalink()); ?>">
            <?php echo esc_html(get_the_title()); ?>
          </a>
        </h2>

        <div>
          <?php the_excerpt(); ?>
        </div>
      </article>

    <?php endwhile; ?>

    <?php the_posts_pagination(); ?>

  <?php else: ?>

    <p>検索結果が見つかりませんでした。</p>

  <?php endif; ?>

</main>

<?php get_footer(); ?>

WordPressの検索結果ページでは、メインクエリが検索結果になっています。

そのため、基本的には WP_Query を新しく書かなくても、have_posts()the_post() で検索結果を表示できます。

カスタム投稿タイプを検索対象に含める

WordPressでカスタム投稿タイプも検索対象に含めたい場合は、functions.php に以下のように書きます。

function my_search_filter($query) {
    if (!is_admin() && $query->is_main_query() && $query->is_search()) {
        $query->set('post_type', ['post', 'page', 'news']);
    }
}
add_action('pre_get_posts', 'my_search_filter');

この例では、検索対象に以下の投稿タイプを含めています。

['post', 'page', 'news']

news はカスタム投稿タイプのスラッグです。

ただし、カスタム投稿タイプを検索対象にするには、登録時の設定も確認してください。

'public' => true,
'exclude_from_search' => false,

exclude_from_searchtrue になっていると、検索対象から除外される場合があります。

PHP検索フォームでよくあるエラー

検索しても結果が表示されない

検索しても結果が表示されない場合は、まずフォームの name とPHP側の受け取り名が一致しているか確認します。

フォーム側が以下の場合、

<input type="search" name="keyword">

PHP側では以下で受け取ります。

$_GET['keyword']

もしフォーム側が、

<input type="search" name="q">

なら、PHP側も以下にする必要があります。

$_GET['q']

フォームの name$_GET のキーが違うと、検索キーワードを正しく受け取れません。

日本語検索がうまくいかない

日本語検索がうまくいかない場合は、文字コードを確認します。

HTMLでは以下を指定します。

<meta charset="UTF-8">

PDO接続では以下を指定します。

charset=utf8mb4

MySQLのデータベース、テーブル、カラムも utf8mb4 にしておくと安心です。

文字コードがずれていると、日本語が文字化けしたり、検索に一致しなかったりする場合があります。

SQLエラーが出る

SQLエラーが出る場合は、開発環境でPDOのエラーモードを確認します。

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

これを指定しておくと、SQLエラーが例外として扱われます。

ただし、本番環境でエラー詳細をそのまま画面に表示するのは避けましょう。

悪い例です。

catch (PDOException $e) {
    echo $e->getMessage();
}

本番環境では、詳細をログに記録し、ユーザーには一般的なエラーメッセージだけを表示します。

catch (PDOException $e) {
    error_log($e->getMessage());
    exit('データベース接続に失敗しました。');
}

検索キーワードがフォームに残らない

検索後も入力欄にキーワードを残したい場合は、value 属性に検索キーワードを入れます。

<input
  type="search"
  name="keyword"
  value="<?php echo h($keyword); ?>"
>

このとき、必ず htmlspecialchars() でエスケープします。

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

検索キーワードはユーザー入力なので、そのままHTMLに出力してはいけません。

ページネーションで検索条件が消える

ページネーションリンクを作るときに、検索キーワードをURLに含め忘れると、2ページ目以降で検索条件が消えてしまいます。

悪い例です。

<a href="?page=2">2</a>

この場合、keyword がURLに含まれていません。

正しくは、以下のように検索条件も含めます。

$query = [
    'keyword' => $keyword,
    'page' => 2,
];

$url = '?' . http_build_query($query);

リンク出力時は、エスケープして表示します。

<a href="<?php echo h($url); ?>">2</a>

PHP検索フォームで意識したいセキュリティ対策

SQLインジェクション対策

SQLインジェクションを防ぐために、ユーザー入力をSQLへ直接連結しないようにします。

避けるべき書き方は以下です。

$sql = "SELECT * FROM articles WHERE title LIKE '%" . $_GET['keyword'] . "%'";

安全に書くには、PDOのプリペアドステートメントを使います。

$stmt = $pdo->prepare(
    'SELECT * FROM articles WHERE title LIKE :keyword'
);

$stmt->bindValue(':keyword', '%' . $keyword . '%', PDO::PARAM_STR);
$stmt->execute();

XSS対策

ユーザーが入力した値や、データベースから取得した値をHTMLに出力するときは、htmlspecialchars() を使います。

echo htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

検索フォームでは、以下のような箇所で必要です。

  • 入力欄の value
  • 検索結果のタイトル
  • 検索結果の本文
  • エラーメッセージ
  • ページネーションリンクのURL

特に、HTML属性に出力するときは忘れずにエスケープしましょう。

入力値のチェック

検索キーワードは、長すぎる文字列が送られる可能性もあります。

そのため、文字数制限を設けると安心です。

if (mb_strlen($keyword, 'UTF-8') > 100) {
    $error = '検索キーワードは100文字以内で入力してください。';
}

また、複数キーワード検索では、キーワード数に上限を設けることも重要です。

$keywords = array_slice($keywords, 0, 5);

これにより、極端に長いSQLが生成されるのを防ぎやすくなります。

並び替え条件はホワイトリストで管理する

検索結果を並び替える場合、ユーザー入力をそのまま ORDER BY に使ってはいけません。

悪い例です。

$order = $_GET['order'] ?? 'created_at';

$sql = "SELECT * FROM articles ORDER BY {$order}";

このような書き方は危険です。

安全にするには、許可した並び替え条件だけを使います。

$allowedOrders = [
    'new' => 'created_at DESC',
    'old' => 'created_at ASC',
    'title' => 'title ASC',
];

$orderKey = $_GET['order'] ?? 'new';

$orderSql = $allowedOrders[$orderKey] ?? $allowedOrders['new'];

$sql = "SELECT id, title, body, created_at
        FROM articles
        ORDER BY {$orderSql}";

プレースホルダで値を渡せるのは、基本的に検索キーワードなどの「値」です。

カラム名やSQL構文そのものをユーザー入力から組み立てる場合は、ホワイトリストで管理する必要があります。

PHP検索フォームの最小構成

まずはこの形をベースにする

PHPとMySQLで検索フォームを作るなら、まずは以下の形をベースにするとよいです。

<?php
require_once 'db.php';

function h($value) {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function escapeLike($value) {
    return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
}

$keyword = $_GET['keyword'] ?? '';

if (!is_string($keyword)) {
    $keyword = '';
}

$keyword = trim($keyword);

$error = '';
$articles = [];

if (mb_strlen($keyword, 'UTF-8') > 100) {
    $error = '検索キーワードは100文字以内で入力してください。';
} elseif ($keyword !== '') {
    $likeKeyword = '%' . escapeLike($keyword) . '%';

    $stmt = $pdo->prepare(
        "SELECT id, title, body, created_at
         FROM articles
         WHERE title LIKE :keyword ESCAPE '\\\\'
            OR body LIKE :keyword ESCAPE '\\\\'
         ORDER BY created_at DESC"
    );

    $stmt->bindValue(':keyword', $likeKeyword, PDO::PARAM_STR);
    $stmt->execute();

    $articles = $stmt->fetchAll();
}
?>

<form action="" method="get">
  <label for="keyword">検索キーワード</label>
  <input
    id="keyword"
    type="search"
    name="keyword"
    value="<?php echo h($keyword); ?>"
    placeholder="キーワードを入力"
  >
  <button type="submit">検索</button>
</form>

<?php if ($error !== ''): ?>

  <p><?php echo h($error); ?></p>

<?php elseif ($keyword === ''): ?>

  <p>検索キーワードを入力してください。</p>

<?php else: ?>

  <h2>「<?php echo h($keyword); ?>」の検索結果</h2>

  <?php if (count($articles) > 0): ?>

    <?php foreach ($articles as $article): ?>
      <article>
        <h3><?php echo h($article['title']); ?></h3>
        <p><?php echo nl2br(h($article['body'])); ?></p>
        <small><?php echo h($article['created_at']); ?></small>
      </article>
    <?php endforeach; ?>

  <?php else: ?>

    <p>検索結果が見つかりませんでした。</p>

  <?php endif; ?>

<?php endif; ?>

このコードでは、以下の対策を入れています。

  • $_GET['keyword'] が配列だった場合の対策
  • trim() による空白除去
  • 文字数制限
  • PDOのプリペアドステートメント
  • LIKE のワイルドカードエスケープ
  • htmlspecialchars() によるXSS対策
  • 検索後も入力欄にキーワードを残す処理

まとめ

PHP検索フォームはHTMLとPHPの連携で作れる

PHPで検索フォームを作る基本は、HTMLフォームでキーワードを送信し、PHPで受け取り、検索対象から一致するデータを取得して表示することです。

最もシンプルなフォームは以下です。

<form action="search.php" method="get">
  <input type="search" name="keyword">
  <button type="submit">検索</button>
</form>

PHP側では、$_GET でキーワードを受け取ります。

$keyword = $_GET['keyword'] ?? '';

MySQLから検索する場合は、LIKE を使います。

WHERE title LIKE '%キーワード%'

ただし、実務ではユーザー入力をSQLに直接連結してはいけません。

PDOのプリペアドステートメントを使い、安全に値を渡します。

$stmt = $pdo->prepare(
    'SELECT * FROM articles WHERE title LIKE :keyword'
);

$stmt->bindValue(':keyword', '%' . $keyword . '%', PDO::PARAM_STR);
$stmt->execute();

また、検索キーワードや検索結果をHTMLに表示するときは、XSS対策として htmlspecialchars() を使います。

echo htmlspecialchars($keyword, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

PHPの検索フォームは、基本だけなら簡単に作れます。

しかし、実務で使うなら以下の点も意識することが大切です。

  • SQLインジェクション対策をする
  • XSS対策をする
  • 検索キーワードの文字数を制限する
  • 複数キーワード検索に対応する
  • 必要に応じてカテゴリー絞り込みを追加する
  • 検索結果が多い場合はページネーションを付ける
  • WordPressでは標準検索機能を活用する

まずは基本の検索フォームを作り、その後に複数キーワード検索やページネーションなどを追加していくと、実用的な検索機能を作りやすくなります。

以上、PHPで検索フォームを作る方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次