🔐

PHP基礎文法とDB接続の安全な書き方|他言語経験者向け

に公開

PHP基礎文法とDB接続の安全な書き方|他言語経験者向け

1. はじめに

前回の記事(DockerでPHP+MySQL開発環境を作る|初めてでも30分で完成)では、DockerでPHP学習環境を構築しました。

記事生成AIとセキュリティの基礎知識DockerでPHP+MySQL開発環境を作るで学んだこと

この記事では、PHPの基本文法を他言語と比較しながら理解 し、安全なDB接続方法 を習得します。JavaScriptやPythonの経験がある方なら、30分でPHPの基本を理解できるはずです。

この記事で実践すること

この記事の目的

  • PHPの基本文法を他言語と比較しながら理解
  • 安全なDB接続方法を習得
  • mysql_*関数が非推奨な理由を理解
  • PDOの正しい使い方を習得
  • SQLインジェクション対策を実践

1.1 このシリーズ全体の流れ

記事 タイトル
生成AIとセキュリティの基礎知識
DockerでPHP+MySQL開発環境を作る
今ここ PHP基礎文法とDB接続の安全な書き方
PHPでTODOリスト作成(CRUDとセキュリティ対策)
手続き型PHPをクラスベースにリファクタリング
PHP実務で使うデバッグとセキュリティチェックリスト
攻撃者の視点で学ぶPHPセキュリティ

2. PHPの特徴を掴む

2.1 PHPはサーバーサイド言語

PHPは、サーバー側で実行される プログラミング言語です。JavaScriptと違って、ブラウザではなくサーバー上で動作 します。

動作の流れ

  1. ブラウザがリクエストを送信
  2. サーバーでPHPコードが実行
  3. 実行結果(HTML)がブラウザに返される

2.2 JavaScript/Pythonとの違い

JavaScript(フロントエンド)

  • ブラウザ上で実行される
  • DOM操作、イベント処理など

Python

  • サーバーサイドでも使えるが、Web開発ではフレームワーク(Django、Flask)が必要
  • データ分析、機械学習などにも使われる

PHP

  • Web開発に特化した言語
  • Apacheに組み込まれて動作(mod_php)
  • 他の言語と比べて、Web開発が簡単

2.3 レガシーコードを読むために知るべきこと

会社の古いPHPシステムを保守する際は、以下の特徴を理解しておく必要があります:

  • 手続き型コードが多い(関数ベース)
  • *mysql_関数が使われている可能性(PHP 7.0で削除済み)
  • セキュリティ対策が不十分な場合がある

この記事では、安全な書き方 を学びながら、レガシーコードも読めるようになります。


3. PHP基本文法|他言語と比較

JavaScriptやPythonの経験がある方なら、PHPの基本文法はすぐに理解できるはずです。主な違いを比較しながら説明します。

3.1 変数と型

PHP

$name = 'やまと';
$age = 27;
$isActive = true;

JavaScript

const name = 'やまと';
const age = 27;
const isActive = true;

主な違い

  • PHPは変数名の前に$が必要
  • 型は緩い(自動変換あり)
  • 最近のPHP(7.4以降)では型宣言も使える

型宣言の例(PHP 7.4以降)

function greet(string $name): string {
    return "こんにちは、{$name}さん";
}

3.2 配列(連想配列がポイント)

PHPの配列で最も重要なのは連想配列です。JavaScriptやPython経験者なら、この概念はすぐに理解できるはずです。

連想配列って何?

連想配列は、数値のインデックスではなく文字列のキーで値を管理できる配列 です。JavaScriptのオブジェクトやPythonの辞書と同じようなものです。

// 連想配列の例
$user = [
    'name' => 'やまと',
    'age' => 27,
    'job' => 'DX specialist'
];

// キーでアクセス
echo $user['name'];  // やまと
echo $user['age'];   // 27

PHPとJavaScriptの違い

PHPの特徴:

  • 配列も連想配列も同じarray
  • 連想配列は['key' => 'value']形式
  • インデックス配列と連想配列を区別しない
// インデックス配列
$foods = ['ラーメン', '寿司', 'カレー'];

// 連想配列
$user = [
    'name' => 'やまと',
    'age' => 27
];

// 両方ともarray型!
var_dump($foods);  // array(3)
var_dump($user);   // array(2)

JavaScriptの特徴:

  • 配列(Array)とオブジェクト(Object)は別の型
  • オブジェクトは{ key: value }形式
// 配列(Array)
const foods = ['ラーメン', '寿司', 'カレー'];

// オブジェクト(Object) - PHPの連想配列に相当
const user = {
    name: 'やまと',
    age: 27
};

// 型が異なる!
console.log(Array.isArray(foods));  // true
console.log(typeof user);            // object

重要なポイント

  • PHPの連想配列 = JavaScriptのオブジェクト = Pythonの辞書
  • PHPは配列も連想配列もarray型として扱う(型の区別がない)
  • JavaScriptはArrayObjectを明確に区別する

実務での使い方

連想配列は、APIの返却値やデータベースからの取得結果を扱う際によく使います:

// APIからユーザー情報を取得した場合
$userData = [
    'id' => 1,
    'name' => 'やまと',
    'email' => 'yamato@example.com',
    'created_at' => '2023-01-15'
];

// キーで直接アクセスできる
echo $userData['name'];  // やまと
echo $userData['email']; // yamato@example.com

連想配列のメリット

  • キーで直接アクセスできるため、データの検索や操作が素早く行える
  • データの追加や削除が容易で、他のデータに影響を与えない
  • 意味のあるキー名を使えるため、コードの可読性が向上する

参考記事:

3.3 関数とクラス(概要)

JavaScriptやPython経験者なら、PHPの関数やクラスの基本構造はすぐに理解できるはずです。主な違いを比較しながら説明します。

関数定義

PHPの関数は、JavaScriptやPythonと似ていますが、いくつか違いがあります。

基本的な関数

function greet($name) {
    return "こんにちは、{$name}さん";
}

echo greet('やまと');  // こんにちは、やまとさん

型宣言付き(PHP 7.0以降、推奨)

function add(int $a, int $b): int {
    return $a + $b;
}

echo add(5, 3);  // 8

JavaScriptとの比較

// JavaScript
function greet(name) {
    return `こんにちは、${name}さん`;
}

// 型宣言(TypeScript)
function add(a: number, b: number): number {
    return a + b;
}

Pythonとの比較

# Python
def greet(name):
    return f"こんにちは、{name}さん"

def add(a: int, b: int) -> int:
    return a + b

重要なポイント

  • PHPは変数名の前に$が必要(関数名には不要)
  • 型宣言はPHP 7.0以降で使える(実務では必須)
  • 返り値の型宣言function name(): type形式
  • 引数の型宣言type $variable形式

実務での使い方

型宣言を使うことで、エラーを早期に発見できます。実務では必須です。

// 型宣言なし(非推奨)
function calculateTotal($price, $quantity) {
    return $price * $quantity;
}

// 型宣言あり(推奨)
function calculateTotal(int $price, int $quantity): int {
    return $price * $quantity;
}

// これで型エラーを防げる
echo calculateTotal(1000, 2);  // 2000
// echo calculateTotal('1000', 2);  // エラー(文字列は不可)

クラス(基本)

PHPのクラスも、JavaScriptやPythonと似た構造です。

class User {
    public $name;
    public $age;
    
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
    
    public function greet(): string {
        return "こんにちは、{$this->name}さん";
    }
}

// インスタンス化
$user = new User('やまと', 27);
echo $user->greet();  // こんにちは、やまとさん

JavaScriptとの比較

// JavaScript (ES6)
class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `こんにちは、${this.name}さん`;
    }
}

重要なポイント

  • $thisでインスタンス自身を参照(JavaScriptと同じ)
  • ->でプロパティやメソッドにアクセス(JavaScriptの.に相当)
  • __constructがコンストラクタ(JavaScriptのconstructorに相当)

注意: クラスの詳細(継承、アクセス修飾子、静的メソッドなど)はPHPのクラスとオブジェクト指向プログラミングで解説します。ここでは基本構造だけ理解しておけばOKです。

3.4 制御構文

PHPの制御構文(if文、for文、foreach文など)は、JavaScriptやPythonとほぼ同じです。特にforeach文はPHPで配列を扱う際に頻繁に使うので、しっかり理解しておきましょう。

if文

条件分岐は、JavaScriptやPythonと同じように書けます。

$age = 25;

if ($age >= 20) {
    echo "成人です";
} elseif ($age >= 13) {
    echo "中高生です";
} else {
    echo "小学生以下です";
}

JavaScriptとの比較

// JavaScript
const age = 25;

if (age >= 20) {
    console.log("成人です");
} else if (age >= 13) {
    console.log("中高生です");
} else {
    console.log("小学生以下です");
}

for文

カウンタを使った繰り返し処理です。

for ($i = 0; $i < 10; $i++) {
    echo $i . '<br>';
}

実務での使い方

// 配列のインデックスでアクセスする場合
$items = ['りんご', 'バナナ', 'オレンジ'];
for ($i = 0; $i < count($items); $i++) {
    echo $items[$i] . '<br>';
}

foreach文(重要・よく使う)

PHPで配列を扱う際、foreach文が最も重要です。配列や連想配列を簡単にループできます。

配列のループ:

$users = ['田中', '佐藤', '鈴木'];
foreach ($users as $user) {
    echo $user . '<br>';
}

連想配列のループ(キーも取得)

$data = ['name' => 'やまと', 'age' => 27];
foreach ($data as $key => $value) {
    echo "{$key}: {$value}<br>";
}
// 出力:
// name: やまと
// age: 27

JavaScriptとの比較

// JavaScript - 配列の場合
const users = ['田中', '佐藤', '鈴木'];
users.forEach(user => {
    console.log(user);
});

// JavaScript - オブジェクトの場合
const data = {name: 'やまと', age: 27};
for (const [key, value] of Object.entries(data)) {
    console.log(`${key}: ${value}`);
}

// または
Object.entries(data).forEach(([key, value]) => {
    console.log(`${key}: ${value}`);
});

実務での使い方

foreach文は、データベースから取得したデータを処理する際によく使います:

// データベースから取得したユーザーリストを処理
$users = [
    ['id' => 1, 'name' => 'やまと', 'email' => 'yamato@example.com'],
    ['id' => 2, 'name' => 'aiko', 'email' => 'aiko@example.com'],
];

foreach ($users as $user) {
    echo "ID: {$user['id']}, 名前: {$user['name']}<br>";
}

重要なポイント

  • foreach文は配列専用のループ構文(最もよく使う)
  • for文はカウンタが必要な場合に使う
  • 連想配列のキーも取得できる(foreach ($array as $key => $value)
  • JavaScriptのforEachfor...ofに相当する機能

3.5 スーパーグローバル変数(PHP独自の仕組み)

スーパーグローバル変数は、PHPがWeb開発に特化している理由の一つです。HTTPリクエストのデータやサーバー情報に簡単にアクセスできます。JavaScriptやPythonのWebフレームワークを使った経験があれば、すぐに理解できるはずです。

スーパーグローバル変数とは

  • PHPがあらかじめ定義している変数
  • プログラムのどこからでもアクセス可能global宣言不要)
  • すべて$_で始まる(命名規則)

主な9種類

変数名 用途 具体例
$_GET GETリクエストデータ $_GET['id'] でURLパラメータ取得
$_POST POSTリクエストデータ $_POST['name'] でフォーム入力取得
$_SERVER サーバー情報 $_SERVER['REMOTE_ADDR'] でIP取得
$_SESSION セッション管理 $_SESSION['user_id'] で状態保持
$_COOKIE クッキー $_COOKIE['token'] でトークン取得
$_FILES ファイルアップロード $_FILES['uploaded']['name']
$_REQUEST GET/POST両方 非推奨(混在を避けるため)
$_ENV 環境変数 $_ENV['API_KEY']
$GLOBALS グローバル変数 $GLOBALS['var']

_GETと_POSTの使い分け

GETリクエスト(URLパラメータ)

// URL: https://example.com/user.php?id=123&name=やまと
$user_id = $_GET['id'];    // 123
$user_name = $_GET['name']; // やまと

POSTリクエスト(フォーム送信)

// HTMLフォーム
// <form method="POST" action="register.php">
//   <input name="email" value="test@example.com">
//   <input name="password" type="password">
// </form>

$email = $_POST['email'];    // test@example.com
$password = $_POST['password']; // 入力されたパスワード

JavaScriptとの比較

// Express.jsの場合
app.get('/user', (req, res) => {
  const id = req.query.id;        // $_GET['id']に相当
  const name = req.query.name;    // $_GET['name']に相当
});

app.post('/user', (req, res) => {
  const email = req.body.email;      // $_POST['email']に相当
  const password = req.body.password; // $_POST['password']に相当
});

実務での使い方

セキュリティに注意: スーパーグローバル変数は、ユーザー入力が含まれるため、必ず検証・サニタイズが必要です。

// 悪い例(セキュリティリスク)
$user_id = $_GET['id'];  // SQLインジェクションの危険性

// 良い例(検証とサニタイズ)
$user_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($user_id <= 0) {
    die('無効なIDです');
}

セッション管理の例

// セッション開始
session_start();

// ログイン時にセッションに保存
$_SESSION['user_id'] = 123;
$_SESSION['user_name'] = 'やまと';

// 別のページで取得
$user_id = $_SESSION['user_id'];   // 123
$user_name = $_SESSION['user_name']; // やまと

重要なポイント

  • $_GETはURLパラメータ$_POSTはフォーム送信で使い分ける
  • $_REQUESTは非推奨(GETとPOSTが混在して危険)
  • すべてユーザー入力なので、必ず検証・サニタイズが必要
  • $_SESSIONでセッション管理が簡単にできる
  • JavaScriptのreq.queryreq.bodyに相当する機能

4. 【実践コラム】生成AIへの質問テクニック

生成AIとセキュリティの基礎知識で学んだ匿名化テクニックを、実際のコードで実践してみましょう。

4.1 匿名化の実例

実際のコードを生成AIに質問する際は、必ず匿名化 する必要があります。

Before(NG例):実際のコードをそのまま貼る

<?php
// 社内の顧客管理システム
$db = new mysqli(
    '192.168.1.100',              // 社内サーバーのIPアドレス
    'admin',                       // 実際のユーザー名
    'P@ssw0rd123',                 // 実際のパスワード
    'customer_orders_db'           // 実際のデータベース名
);

$query = "
    SELECT 
        seihin_master.seihin_code,
        seihin_master.patent_no,
        kokyaku_order.naibu_price
    FROM seihin_master
    INNER JOIN kokyaku_order 
        ON seihin_master.seihin_code = kokyaku_order.seihin_code
    WHERE kokyaku_order.customer_type = 'VIP'
";
$result = $db->query($query);
?>

なぜNGか

  • IPアドレスから社内サーバーの場所が分かる
  • テーブル名(seihin_masterkokyaku_order)から業務内容が推測できる
  • カラム名(patent_nonaibu_price)から機密情報が推測できる

After(OK例):匿名化済み

<?php
// 商品管理システム
$db = new mysqli(
    'localhost',                   // 汎用的なホスト名
    'dbuser',                      // ダミーのユーザー名
    'password',                    // ダミーのパスワード
    'mydb'                         // 汎用的なデータベース名
);

$query = "
    SELECT 
        products.product_code,
        products.reference_no,
        orders.price
    FROM products
    INNER JOIN orders 
        ON products.product_code = orders.product_code
    WHERE orders.customer_type = 'VIP'
";
$result = $db->query($query);
?>

匿名化のポイント

  • 192.168.1.100localhost
  • admin / P@ssw0rd123dbuser / password
  • customer_orders_dbmydb
  • seihin_masterproducts(英語の汎用名)
  • patent_noreference_no(特許番号であることを隠す)
  • naibu_priceprice(内部価格であることを隠す)

4.2 Before/Afterの比較

質問例:NG例とOK例

❌ NG例:具体的すぎる質問

「うちの会社のusersテーブル(user_id, email, password, created_at)に
接続するコードを書いて」

→ 実際のテーブル構造を晒している

✅ OK例:抽象化した質問

「TODOリストのtodosテーブル(id, title, status, created_at)に
接続するサンプルコードを教えて」

→ 完全にダミーのデータ構造

4.3 質問の仕方の工夫

実際のテーブル構造や業務ロジックを晒さずに、抽象化した質問をすることで、セキュリティを守りながら効果的に学べます。

抽象化のコツ

  1. テーブル名を汎用的な名前に置き換える

    • customer_ordersorders
    • seihin_masterproducts
  2. カラム名を汎用的な名前に置き換える

    • patent_noreference_no
    • naibu_priceprice
  3. 業務ロジックを伏せる

    • 「特許番号で検索」→「参照番号で検索」
    • 「内部価格を計算」→「価格を計算」

4.4 質問例と回答例

質問

「PDOでtodosテーブル(id, title, status)から
statusが0のレコードを取得するコードを教えてください。
Docker環境で、MySQLコンテナのサービス名はmysqlです。」

回答(生成AIから)

<?php
try {
    $pdo = new PDO(
        'mysql:host=mysql;dbname=mydb;charset=utf8mb4',
        'dbuser',
        'dbpass',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
    
    $stmt = $pdo->prepare('SELECT * FROM todos WHERE status = :status');
    $stmt->bindValue(':status', 0, PDO::PARAM_INT);
    $stmt->execute();
    
    $todos = $stmt->fetchAll();
    
    foreach ($todos as $todo) {
        echo "ID: {$todo['id']}, Title: {$todo['title']}<br>";
    }
} catch (PDOException $e) {
    echo "エラー: " . $e->getMessage();
}
?>

ポイント

  • 完全にダミーのデータ構造
  • 実際の業務コードは一切使っていない
  • セキュリティを守りながら学習できる

5. なぜmysql_*は使わないのか?

レガシーコードでは、mysql_connect()mysql_query()などのmysql_*関数が使われていることがあります。しかし、これらの関数はPHP 7.0で完全に削除されました。JavaScriptやPython経験者なら、古いAPIが非推奨になることは理解できるはずです。PHPでも同じように、セキュリティと機能性の観点から新しい方法に移行する必要があります。

5.1 mysql_*関数が非推奨な理由

廃止の経緯

  • PHP 5.5.0(2013年6月):正式に非推奨(E_DEPRECATED警告)
  • PHP 7.0.0(2015年12月):完全削除

現在(2024年)では、PHP 7.0未満のサポートは終了しているため、mysql_*関数は使えません。

廃止された3つの理由

1. セキュリティリスク(最重要)

プリペアドステートメント非対応

  • SQLインジェクション対策は手動エスケープのみ(mysql_real_escape_string
  • 手動処理はミスが多く、エスケープ漏れのリスクが高い

NG例(SQLインジェクションの危険)

// ❌ NG: ユーザー入力をそのまま埋め込む
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysql_query($sql);

// 攻撃例: id = "1 OR 1=1" と入力されると全データが取得される

JavaScriptとの比較

// JavaScriptでも同様の危険性がある
// ❌ NG: SQLインジェクションの危険(Node.jsの例)
const id = req.query.id;
const sql = `SELECT * FROM users WHERE id = ${id}`;
// これも危険!プリペアドステートメントを使うべき

2. 機能不足

  • MySQL 3.23向けに作られた古い設計(1990年代後半)
  • ストアドプロシージャ非対応
  • トランザクション機能が限定的
  • 文字セット・照合順序の完全対応なし

3. 新しいMySQL認証方式に非対応

  • MySQL 5.6以降のデフォルト認証方式に未対応
  • OLD_PASSWORD形式のみサポート

5.2 レガシーコードでよく見るNG例

レガシーコードを読む際は、以下のパターンに注意してください。

パターン1:mysql_connect(接続方法)

// ❌ NG: PHP 7.0で動かない
$con = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('mydb', $con);

パターン2:mysql_query + 文字列結合(SQLインジェクションの危険)

// ❌ NG: 最も危険なパターン
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysql_query($sql);

パターン3:mysql_real_escape_string(エスケープ漏れのリスク)

// ❌ NG: エスケープ漏れのリスクがある
$name = mysql_real_escape_string($_POST['name']);
$sql = "INSERT INTO users (name) VALUES ('$name')";
mysql_query($sql);

// 問題点: 手動エスケープはミスが起きやすい
// 例: 数値の場合はエスケープ不要だが、文字列と混同しやすい

5.3 移行の必要性

レガシーコードでmysql_*関数を見かけたら、すぐにPDOまたはMySQLiに移行する必要があります。

移行が必要な理由

  • PHP 7.0以降では、これらの関数は存在しないため、コードが動きません
  • セキュリティリスクが高いため、放置できない
  • 新しい機能(トランザクション、ストアドプロシージャなど)が使えない

移行先の選択

  • PDO:複数のデータベースに対応、オブジェクト指向(推奨)
  • MySQLi:MySQL専用、手続き型とオブジェクト指向の両方に対応

実務での対応

// 古いコード(mysql_*)を見つけたら
// ❌ 古いコード
$result = mysql_query("SELECT * FROM users WHERE id = $id");

// ✅ 新しいコード(PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll();

重要なポイント

  • mysql_*関数はPHP 7.0で削除済み(現在は使えない)
  • セキュリティリスクが高い(SQLインジェクション対策が不十分)
  • 機能が限定的(トランザクション、ストアドプロシージャ非対応)
  • PDOまたはMySQLiへの移行が必須

6. PDOによる安全なDB接続

PDO(PHP Data Objects)は、PHP 5.1以降の標準DB接続クラスです。プリペアドステートメントによりSQLインジェクション対策が自動化されます。JavaScript経験者なら、Node.jsのmysql2pgパッケージのプリペアドステートメントと似た概念です。Python経験者なら、psycopg2sqlite3のパラメータ化クエリと同じ考え方です。

6.1 PDOとは何か

PDOは、PHPでデータベースに接続するための統一されたインターフェースです。複数のデータベースに対応し、同じAPIで操作できます。

PDOの特徴

  • 複数のデータベースに対応(MySQL、PostgreSQL、SQLiteなど)
  • プリペアドステートメントでセキュリティ対策が自動化
  • オブジェクト指向のAPI(クラスベース)

JavaScriptとの比較

// Node.js (mysql2)の場合
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'user',
    password: 'pass',
    database: 'mydb'
});

// プリペアドステートメント
const [rows] = await connection.execute(
    'SELECT * FROM users WHERE id = ?',
    [userId]
);

Pythonとの比較

# Python (psycopg2)の場合
import psycopg2

conn = psycopg2.connect(
    host='localhost',
    user='user',
    password='pass',
    database='mydb'
)

# プリペアドステートメント
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))

6.2 DB接続の書き方

基本的な接続コード

<?php
try {
    $pdo = new PDO(
        'mysql:host=mysql;dbname=mydb;charset=utf8mb4',
        'dbuser',
        'dbpass',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
} catch (PDOException $e) {
    exit('接続失敗: ' . $e->getMessage());
}
?>

ポイント

  • DSN形式mysql:host=ホスト名;dbname=DB名;charset=utf8mb4
  • Docker環境ではhost=mysql(サービス名で接続)
  • charset=utf8mb4を必ず指定(文字化け防止)
  • 接続失敗時は必ずPDOExceptionがスローされる

実務での使い方

環境変数を使った接続(推奨)

<?php
// 環境変数から取得(セキュリティベストプラクティス)
$host = $_ENV['DB_HOST'] ?? 'localhost';
$dbname = $_ENV['DB_NAME'] ?? 'mydb';
$user = $_ENV['DB_USER'] ?? 'user';
$pass = $_ENV['DB_PASS'] ?? 'pass';

try {
    $pdo = new PDO(
        "mysql:host={$host};dbname={$dbname};charset=utf8mb4",
        $user,
        $pass,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
} catch (PDOException $e) {
    error_log('DB接続エラー: ' . $e->getMessage());
    exit('データベースに接続できませんでした');
}
?>

6.3 エラーモードの設定

エラーモードは、PDOでエラーが発生した際の動作を制御します。実務では必ずEXCEPTIONモードに設定しましょう。

エラーモード3種類

モード 定数 動作 PHP 8.0以降
SILENT PDO::ERRMODE_SILENT エラーコードのみ設定、例外もwarningも出さない -
WARNING PDO::ERRMODE_WARNING E_WARNING発生、スクリプトは継続 -
EXCEPTION PDO::ERRMODE_EXCEPTION PDOException発生、スクリプト停止 デフォルト

推奨設定:PDO::ERRMODE_EXCEPTION

なぜEXCEPTIONモードが推奨なのか

  • エラーを見逃さない:SILENTモードだとエラーに気づかない
  • try-catchで適切な処理が可能:エラーハンドリングが明確
  • デバッグが容易:エラーが発生した場所が明確

設定方法

// 接続後に設定
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// または接続時に設定(推奨)
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

実務での使い方

<?php
try {
    $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);
    
    // SQL実行
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
    $stmt->execute([':id' => $user_id]);
    
} catch (PDOException $e) {
    // エラーログに記録(本番環境では詳細情報を出さない)
    error_log('DBエラー: ' . $e->getMessage());
    
    // ユーザーには分かりやすいメッセージを表示
    echo 'データの取得に失敗しました。';
    exit;
}
?>

6.4 プリペアドステートメントの基本

プリペアドステートメントは、SQLインジェクションを防ぐための最も重要な機能 です。JavaScriptやPython経験者なら、パラメータ化クエリの概念は理解できるはずです。

プリペアドステートメントとは

SQL文とパラメータを分離して実行する仕組みです。ユーザー入力をSQL文に直接埋め込まず、プレースホルダ(?:name)を使います。

メリット:

  • SQLインジェクション対策が自動化(最も重要)
  • パフォーマンス向上(SQL文の解析が1回だけ)
  • 型安全性(データ型を明示的に指定)

処理の流れ

  1. prepare() - SQL文を準備(プレースホルダを含む)
  2. bindValue() or bindParam() - パラメータをバインド
  3. execute() - 実行
  4. fetch() or fetchAll() - 結果取得(SELECT時)

SELECT例(名前付きプレースホルダ)

<?php
// 名前付きプレースホルダ(推奨:可読性が高い)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', 5, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// 結果の使用例
if ($user) {
    echo "名前: {$user['name']}";
}
?>

INSERT例(疑問符プレースホルダ)

<?php
// 疑問符プレースホルダ(シンプル)
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->bindValue(1, 'Taro', PDO::PARAM_STR);
$stmt->bindValue(2, 'taro@example.com', PDO::PARAM_STR);
$stmt->execute();

// 挿入されたIDを取得
$new_id = $pdo->lastInsertId();
?>

プレースホルダの種類

種類 記法 メリット デメリット
名前付き :name 可読性が高い、順序を気にしなくてOK 少し長い
疑問符 ? シンプル、短い 位置で指定(1から始まる)

推奨:名前付きプレースホルダ(可読性が高いため)

注意: 名前付きと疑問符を同じSQL文で混在させることはできません。

JavaScriptとの比較

// Node.js (mysql2)の場合
const [rows] = await connection.execute(
    'SELECT * FROM users WHERE id = ?',
    [userId]
);

// 名前付きパラメータ(mysql2では非標準)
const [rows] = await connection.execute(
    'SELECT * FROM users WHERE id = :id',
    { id: userId }
);

Pythonとの比較

# Python (psycopg2)の場合
cursor.execute(
    'SELECT * FROM users WHERE id = %s',
    (user_id,)
)

# 名前付きパラメータ
cursor.execute(
    'SELECT * FROM users WHERE id = %(id)s',
    {'id': user_id}
)

実務での使い方

複数のパラメータを使う場合

<?php
// 名前付きプレースホルダの方が分かりやすい
$stmt = $pdo->prepare('
    SELECT * FROM users 
    WHERE name = :name AND age >= :min_age
');
$stmt->bindValue(':name', 'やまと', PDO::PARAM_STR);
$stmt->bindValue(':min_age', 20, PDO::PARAM_INT);
$stmt->execute();
$users = $stmt->fetchAll();
?>

6.5 bindParam vs bindValue の違い

bindParambindValueは、パラメータをバインドする2つの方法です。通常はbindValueを使います。違いを理解しておくと、バグを防げます。

項目 bindParam bindValue
渡し方 参照渡し 値渡し
評価タイミング execute()時 bindValue()時
値の変更 execute前に変更すると反映される bind後の変更は反映されない
推奨度 △(特殊ケースのみ) ✅(通常はこちら)

bindValue例(推奨)

<?php
$stmt = $pdo->prepare('SELECT * FROM users WHERE status = :status');
$status = 'active';
$stmt->bindValue(':status', $status, PDO::PARAM_STR);

// この後$statusを変更しても影響なし
$status = 'inactive';
$stmt->execute(); // WHERE status = 'active' で実行される
?>

なぜbindValueが推奨なのか

  • 値が確定するので予測しやすい:実行時に値が変わらない
  • ループ内でのバグを避けられる:参照渡しによる予期しない動作を防げる
  • 参照を扱わないので混乱しない:シンプルで理解しやすい

bindParam例(参照渡し)

<?php
$stmt = $pdo->prepare('SELECT * FROM users WHERE status = :status');
$status = 'active';
$stmt->bindParam(':status', $status, PDO::PARAM_STR);

// この変更が反映される!
$status = 'inactive';
$stmt->execute(); // WHERE status = 'inactive' で実行される
?>

bindParamを使うケース

  • ループ内で同じステートメントを複数回実行する場合(パフォーマンス最適化)
  • 実行時に値を変更する必要がある特殊なケース

実務での使い方

<?php
// ✅ 通常はbindValueを使う
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $user_id, PDO::PARAM_INT);
$stmt->execute();

// △ bindParamを使う特殊なケース(ループ内で再利用)
$stmt = $pdo->prepare('INSERT INTO logs (message) VALUES (:message)');
$stmt->bindParam(':message', $message, PDO::PARAM_STR);

foreach ($messages as $message) {
    $stmt->execute(); // $messageの値が毎回変わる
}
?>

6.6 データ型の指定

データ型を明示的に指定することで、型安全性が向上し、予期しない動作を防げます。

第3引数のデータ型定数

定数 使用例
PDO::PARAM_INT 整数 ID、年齢、数値カウント
PDO::PARAM_STR 文字列(デフォルト) 名前、メールアドレス、テキスト
PDO::PARAM_BOOL 真偽値 フラグ、有効/無効
PDO::PARAM_NULL NULL オプショナルな値

指定しない場合はPDO::PARAM_STRになる

整数の場合は必ずPDO::PARAM_INTを指定

<?php
// ✅ 正しい:整数として明示的に指定
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', 123, PDO::PARAM_INT);
$stmt->execute();

// ❌ 間違い:文字列として扱われる可能性がある
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', 123); // PDO::PARAM_STRとして扱われる
$stmt->execute();

実務での使い方

型を正しく指定する例:

<?php
// ユーザー情報を取得
$stmt = $pdo->prepare('
    SELECT * FROM users 
    WHERE id = :id AND age >= :min_age AND is_active = :is_active
');
$stmt->bindValue(':id', $user_id, PDO::PARAM_INT);        // 整数
$stmt->bindValue(':min_age', 18, PDO::PARAM_INT);         // 整数
$stmt->bindValue(':is_active', true, PDO::PARAM_BOOL);     // 真偽値
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// ユーザーを登録
$stmt = $pdo->prepare('
    INSERT INTO users (name, email, age) 
    VALUES (:name, :email, :age)
');
$stmt->bindValue(':name', $name, PDO::PARAM_STR);          // 文字列
$stmt->bindValue(':email', $email, PDO::PARAM_STR);        // 文字列
$stmt->bindValue(':age', $age, PDO::PARAM_INT);            // 整数
$stmt->execute();
?>

重要なポイント

  • 整数は必ずPDO::PARAM_INTを指定(パフォーマンスと型安全性のため)
  • 文字列はPDO::PARAM_STRを明示的に指定(デフォルトだが明示的に書く方が良い)
  • 真偽値はPDO::PARAM_BOOLを指定(データベースによって扱いが異なる場合がある)
  • NULL値はPDO::PARAM_NULLを指定(オプショナルな値の場合)

7. SQLインジェクションとは

SQLインジェクションは、ユーザー入力をそのままSQL文に埋め込むことで発生する脆弱性です。2023年1-3月だけで約1,700万件の攻撃が検知され、前年比210%増と急増しています。JavaScriptやPython経験者なら、パラメータ化クエリの重要性は理解できるはずです。PHPでも同じように、プリペアドステートメントを使うことで防げる 脆弱性です。

7.1 SQLインジェクションの仕組み

SQLインジェクションは、ユーザー入力がSQL文の構造を変更してしまう ことで発生します。JavaScriptやPythonでも同様の脆弱性があるため、理解しやすいはずです。

危険なコード例

// ❌ NG例: ユーザー入力をそのまま埋め込む
$user_id = $_GET['id']; // 例: "taro"
$sql = "SELECT * FROM users WHERE id = '$user_id'";
// 実際のSQL: SELECT * FROM users WHERE id = 'taro'

攻撃された場合

攻撃者がidパラメータにtaro' or '1'='1を入力すると:

$user_id = "taro' or '1'='1"; // 攻撃者の入力
$sql = "SELECT * FROM users WHERE id = '$user_id'";
// 実際のSQL: SELECT * FROM users WHERE id = 'taro' or '1'='1'

何が起きるか?

  • WHERE id = 'taro' → taroのデータを取得
  • または
  • '1'='1' → 常にtrue(すべての行にマッチ)

結果:全ユーザーのデータが取得される

JavaScriptとの比較

// ❌ NG: Node.jsでも同様の危険性がある
const userId = req.query.id; // "taro' or '1'='1"
const sql = `SELECT * FROM users WHERE id = '${userId}'`;
// これもSQLインジェクションの危険性がある

// ✅ OK: プリペアドステートメントを使う
const [rows] = await connection.execute(
    'SELECT * FROM users WHERE id = ?',
    [userId]
);

Pythonとの比較

# ❌ NG: Pythonでも同様の危険性がある
user_id = request.args.get('id')  # "taro' or '1'='1"
sql = f"SELECT * FROM users WHERE id = '{user_id}'"
# これもSQLインジェクションの危険性がある

# ✅ OK: パラメータ化クエリを使う
cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))

別の攻撃パターン(データ削除)

// 攻撃者の入力: taro'; DROP TABLE users;--
$sql = "SELECT * FROM users WHERE id = 'taro'; DROP TABLE users;--'";
// 実際に実行される:
// 1. SELECT * FROM users WHERE id = 'taro';
// 2. DROP TABLE users; ← テーブル削除(最悪のケース)
// 3. -- 以降はコメントアウト

この攻撃の影響:

  • データベースのテーブルが削除される
  • アプリケーションが完全に動作不能になる
  • データの復旧が困難

よく使われる攻撃パターン

攻撃パターン 入力例 目的
認証回避 ' or '1'='1 パスワード不要でログイン
データ抽出 ' UNION SELECT * FROM passwords-- 別テーブルのデータ取得
データ削除 '; DROP TABLE users;-- テーブル削除
データ改ざん '; UPDATE users SET password='hacked'-- パスワード変更

7.2 プリペアドステートメントがなぜ防げるか

プリペアドステートメントは、SQL文の構造とデータを分離 することで、SQLインジェクションを防ぎます。JavaScriptやPython経験者なら、パラメータ化クエリの仕組みは理解できるはずです。

安全な書き方(PDO)

// ✅ OK例: プリペアドステートメント
$user_id = $_GET['id']; // 例: "taro' or '1'='1"

// SQL文の構造を先に定義(プレースホルダ)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");

// 値を後からバインド(SQL文の構造は変わらない)
$stmt->bindValue(':id', $user_id, PDO::PARAM_STR);
$stmt->execute();

防げる理由

  1. SQL文の構造が先に確定するprepare()の時点でSQL文の構造が固定される
  2. :idに入る値は「文字列データ」として扱われる:SQL文の一部ではなく、データとして扱われる
  3. 攻撃者が' or '1'='1を入力しても、それは単なる文字列として検索される:SQL文の構造を変更できない
  4. SQL文の構造自体を変更することは理論的に不可能:データベースエンジンが自動的にエスケープ処理を行う

実際の動作

-- 攻撃者が "taro' or '1'='1" を入力した場合
SELECT * FROM users WHERE id = 'taro\' or \'1\'=\'1\'';
-- ↑ シングルクォートがエスケープされる(無害化)
-- 結果: "taro' or '1'='1" という名前のユーザーを検索(該当なし)

Before/Afterの比較

Before(危険なコード)

// ❌ NG: ユーザー入力をそのまま埋め込む
$user_id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = '$user_id'";
$result = mysql_query($sql); // または $pdo->query($sql)

After(安全なコード)

// ✅ OK: プリペアドステートメントを使う
$user_id = $_GET['id'];
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $user_id, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetchAll();

実務での使い方

すべてのユーザー入力をプリペアドステートメントで処理

<?php
// ユーザー検索機能
$search_name = $_GET['name'] ?? '';
$min_age = $_GET['min_age'] ?? 0;

// ✅ OK: すべてのパラメータをプリペアドステートメントで処理
$stmt = $pdo->prepare('
    SELECT * FROM users 
    WHERE name LIKE :name AND age >= :min_age
');
$stmt->bindValue(':name', "%{$search_name}%", PDO::PARAM_STR);
$stmt->bindValue(':min_age', $min_age, PDO::PARAM_INT);
$stmt->execute();
$users = $stmt->fetchAll();
?>

重要なポイント

  • プリペアドステートメントは必須:ユーザー入力を使うSQL文は必ずプリペアドステートメントを使う
  • エスケープ処理は不要:プリペアドステートメントが自動的に処理する
  • すべてのデータベース操作に適用:SELECT、INSERT、UPDATE、DELETEすべてに使う
  • パフォーマンスも向上:SQL文の解析が1回だけなので高速

7.3 実際の被害事例

SQLインジェクションは、実際に大規模な被害を引き起こしている 深刻な脆弱性です。以下の事例から、対策の重要性が理解できるはずです。

事例1:大手ゲーム会社(2011年)

  • 被害規模:7,700万件の個人情報漏洩
  • 原因:アプリケーションサーバーの脆弱性(SQLインジェクションと推測)
  • 影響:クレジットカード情報を含む大規模漏洩
  • 教訓:大企業でも対策が不十分だと大規模な被害が発生する

事例2:リサーチ会社(2022年6月)

  • 被害規模:最大10万2,000件の会員情報流出
  • 原因:SQLインジェクション脆弱性
  • 対応
    • 被害会員への個別連絡
    • 運営サイトの一時閉鎖
    • パスワードの初期化
    • 問い合わせ窓口の設置
    • 脆弱性防御ツールの導入

教訓:被害が発生してから対応するのではなく、事前に予防することが重要

実務での対策

これらの事例から学ぶべきことは、プリペアドステートメントを必ず使う ことです。たとえ小さなアプリケーションでも、SQLインジェクション対策は必須です。

<?php
// ✅ 実務では必ずこのパターンを使う
function getUserById(PDO $pdo, int $user_id): ?array {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
    $stmt->bindValue(':id', $user_id, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}

// 使用例
$user = getUserById($pdo, $_GET['id']);
if ($user) {
    echo "名前: {$user['name']}";
}
?>

7.4 まとめ:SQLインジェクション対策のチェックリスト

実務でSQLインジェクションを防ぐためのチェックリストです。

✅ 必ず守るべきこと

  • すべてのユーザー入力をプリペアドステートメントで処理
  • 文字列結合でSQL文を作らない"SELECT * FROM users WHERE id = $id"はNG)
  • PDOまたはMySQLiを使うmysql_*関数は使わない)
  • データ型を明示的に指定PDO::PARAM_INTPDO::PARAM_STRなど)

❌ 絶対にやってはいけないこと

  • ユーザー入力をそのままSQL文に埋め込む
  • mysql_real_escape_stringに頼る(プリペアドステートメントを使う)
  • エスケープ処理を手動で行う(プリペアドステートメントが自動処理)

実務でのベストプラクティス

<?php
// ✅ 推奨パターン:関数化して再利用
class UserRepository {
    private PDO $pdo;
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    public function findById(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    
    public function searchByName(string $name): array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE name LIKE :name');
        $stmt->bindValue(':name', "%{$name}%", PDO::PARAM_STR);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}
?>

重要なポイント

  • SQLインジェクションは防げる脆弱性:プリペアドステートメントを使えば100%防げる
  • 実務では必須の対策:ユーザー入力を使うSQL文は必ずプリペアドステートメントを使う
  • パフォーマンスも向上:SQL文の解析が1回だけなので高速
  • コードの可読性も向上:SQL文とデータが分離されて読みやすい

参考記事:


8. TODOテーブルを設計する

実際のテーブル設計を体験しましょう。TODOリストに必要なカラムを考えて、CREATE TABLE文を作成します。

8.1 TODOリストに必要なカラム

最小限の構成(学習用おすすめ)

CREATE TABLE todos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    status TINYINT DEFAULT 0 COMMENT '0:未完了 1:完了',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

8.2 データ型の選択

各カラムの説明

カラム名 データ型 説明
id INT AUTO_INCREMENT PRIMARY KEY 主キー、自動採番
title VARCHAR(255) NOT NULL タスクのタイトル
status TINYINT DEFAULT 0 0:未完了、1:完了
created_at DATETIME DEFAULT CURRENT_TIMESTAMP 作成日時(自動設定)
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 更新日時(自動更新)

8.3 AUTO_INCREMENTの重要ポイント

設定時の制約

  • テーブルごとに1つのカラムにしか設定できない
  • PRIMARY KEYまたはUNIQUE KEYである必要がある
  • 整数型(INT、BIGINT)にのみ設定可能
  • DEFAULT制約は設定できない

動作の特徴

  • データ削除後も連番は戻らない(例:id=3を削除しても次は4)
  • 値は過去の最大値+1で自動採番される
  • 明示的に値を指定することも可能(ただし最大値より大きい値のみ)

8.4 初期データINSERT

ダミーデータの例

INSERT INTO todos (title, status) VALUES
    ('PHP公式ドキュメントを読む', 1),
    ('PDOの基本を理解する', 1),
    ('SQLインジェクション対策を学ぶ', 0),
    ('TODOアプリを作る', 0),
    ('生成AIにコードレビューを依頼する', 0);

9. 実際にDB接続してみる

学んだことを実践してみましょう。PDOでTODOテーブルに接続して、データを取得します。

9.1 接続確認のコード

www/db_test.phpを作成

<?php
try {
    // PDO接続
    $pdo = new PDO(
        'mysql:host=mysql;dbname=mydb;charset=utf8mb4',
        'dbuser',
        'dbpass',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
    
    echo "✅ データベース接続成功!<br><br>";
    
    // テーブル一覧を取得
    $stmt = $pdo->query("SHOW TABLES");
    $tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
    
    echo "テーブル一覧:<br>";
    foreach ($tables as $table) {
        echo "- {$table}<br>";
    }
    
} catch (PDOException $e) {
    echo "❌ エラー: " . $e->getMessage();
}
?>

ブラウザでhttp://localhost:8080/db_test.phpにアクセスして、接続が成功するか確認してください。

9.2 データ取得(SELECT)

www/todos.phpを作成

<?php
try {
    $pdo = new PDO(
        'mysql:host=mysql;dbname=mydb;charset=utf8mb4',
        'dbuser',
        'dbpass',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
    
    // プリペアドステートメントでSELECT
    $stmt = $pdo->prepare('SELECT * FROM todos WHERE status = :status');
    $stmt->bindValue(':status', 0, PDO::PARAM_INT);
    $stmt->execute();
    
    $todos = $stmt->fetchAll();
    
    echo "<h1>未完了のTODO</h1>";
    echo "<ul>";
    foreach ($todos as $todo) {
        echo "<li>ID: {$todo['id']}, Title: {$todo['title']}</li>";
    }
    echo "</ul>";
    
} catch (PDOException $e) {
    echo "❌ エラー: " . $e->getMessage();
}
?>

ブラウザでhttp://localhost:8080/todos.phpにアクセスして、データが表示されるか確認してください。

9.3 よくあるエラーと対処法

9.3.1 エラー1:接続エラー

症状

SQLSTATE[HY000] [2002] Connection refused

原因

  • MySQLコンテナが起動していない
  • ホスト名が間違っている(localhostではなくmysqlを使う)

対処法

# コンテナの状態を確認
docker-compose ps

# MySQLコンテナが起動していない場合
docker-compose up -d mysql

9.3.2 エラー2:データベースが見つからない

症状

SQLSTATE[HY000] [1049] Unknown database 'mydb'

原因:データベースが作成されていない

対処法:phpMyAdmin(http://localhost:8081)にログインして、mydbデータベースを作成してください。

または、SQLで作成:

CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

9.3.3 エラー3:テーブルが見つからない

症状

SQLSTATE[42S02] Base table or view not found: 1146 Table 'mydb.todos' doesn't exist

原因todosテーブルが作成されていない

対処法:phpMyAdminでCREATE TABLE文を実行するか、SQLファイルを作成して実行してください。


10. まとめ|次のステップ

おめでとうございます!PHPの基礎文法とDB接続の安全な書き方を学びました。

10.1 この記事で学んだこと

  • PHPの基本文法を他言語と比較しながら理解
  • スーパーグローバル変数($_GET$_POST)の使い方
  • mysql_*関数が非推奨な理由
  • PDOによる安全なDB接続
  • プリペアドステートメントでSQLインジェクション対策
  • TODOテーブルの設計とデータ取得

10.2 セキュリティを意識した書き方

  • ✅ PDO + プリペアドステートメントを使う
  • ✅ エラーモードをERRMODE_EXCEPTIONに設定
  • bindValueでデータ型を指定
  • ❌ 文字列連結でSQL文を作らない
  • ❌ mysql_*関数は使わない

10.3 次のステップ

次回は、CRUD操作を実装します。TODOリストの作成・読み取り・更新・削除を実装しながら、XSS対策やバリデーションも学びます。

10.4 困った時は

  • エラーが出たら、まずdocker-compose logs -f mysqlでログを確認
  • 詰まったら、生成AIに質問してみましょう(ただし、セキュリティは守って)
  • 記事生成AIとセキュリティの基礎知識の匿名化チェックリストを確認

11. 参考資料

11.1 PHP公式ドキュメント

11.2 信頼できる解説記事


12. 📚 シリーズ記事一覧

この記事: PHP基礎文法とDB接続の安全な書き方

前の記事: DockerでPHP+MySQL開発環境を作る
→ 本番環境とは完全に分離されたローカル環境を構築します。

次の記事: PHPでTODOリスト作成(CRUDとセキュリティ対策)
→ TODOリストのCRUD操作を実装しながら、XSS対策やバリデーションも学びます。

シリーズ全体

  1. 生成AIとセキュリティの基礎知識
  2. DockerでPHP+MySQL開発環境を作る
  3. PHP基礎文法とDB接続の安全な書き方 ← 今ここ
  4. PHPでTODOリスト作成(CRUDとセキュリティ対策)
  5. 手続き型PHPをクラスベースにリファクタリング
  6. PHP実務で使うデバッグとセキュリティチェックリスト
  7. 攻撃者の視点で学ぶPHPセキュリティ

Discussion