commit a656ab0219d3dd5441dc57064a6964901f014ab5 Author: a-kizaki Date: Thu Oct 29 08:36:44 2026 +0900 no message diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..45640d9 --- /dev/null +++ b/Readme.md @@ -0,0 +1,161 @@ +# PHP・DB連携 総合演習課題 + +## はじめに(演習の⽬的と取り組み⽅) +当演習は、講義で学んだことを⾃⼒で実践できるようになるために実施します。 そのために以下の内容を守って演習に取り組んでください。 + +### 納品物、納期を守りましょう +この演習は、実際の仕事を想定した内容です。 +提出物の内容と提出期限は必ず守ってください。 +作業が間に合わない、提出物に問題が出そうな場合は、**必ずルールにしたがって適切な相⼿に相談してください**。 + +### 適切な⼿順で開発しましょう +プログラムを書き始める前に、**README.md** や**仕様書**(配布されている場合) を必ず読んでください。 +「**何を作るのか**」、「**どこまで作ればよいのか**」を理解してから作業を始めてください。 + +### 知らないことは⾃分で調べましょう +この演習には、**講義で扱っていない内容が⼀部含まれています**。 +その場合は、インターネットなどで⾃分で調べながら進めてください。 +調べることも、エンジニアにとって⼤切なスキルの⼀つです。 + + +## 演習目的 + +この演習では、PHPの基礎知識とオブジェクト指向プログラミング(OOP)、そしてMySQLデータベースとの連携を総合的に学習します。 + +### 学習ポイント +- **クラスとインスタンス**: オブジェクト指向の基本概念を実装する +- **カプセル化**: データの隠蔽とアクセス制御を実践する +- **インタフェース**: 契約に基づいた設計を理解する +- **多態性(ポリモーフィズム)**: インタフェースを通じた柔軟な実装 +- **例外処理**: エラーハンドリングの適切な実装 +- **CRUD操作**: データベースの基本操作(Create, Read, Update, Delete) +- **JOIN操作**: 複数テーブルを結合したデータ取得 + +## フォルダ構成 + +``` +PHP-EM-OOP-DB/ +│ +├── README.md # このファイル +├── start-DDL.sql # データベース・テーブル作成SQL +├── start-DML.sql # 初期データ投入SQL +│ +├── config/ +│ └── db_config.php # データベース接続設定 +│ +├── interfaces/ +│ └── DataAccessInterface.php # データアクセスインタフェース定義 +│ +├── database/ +│ ├── abstracts/ +│ │ └── BaseAccess.php # データベース接続基底クラス +│ └── products/ +│ └── ProductAccess.php # 商品データアクセスクラス +│ +└── public/ + ├── index.php # トップページ + ├── product_list.php # 商品一覧表示(実装済み) + ├── product_register.php # 商品登録処理 + ├── product_update.php # 商品更新処理 + ├── product_delete.php # 商品削除処理 + └── product_join.php # JOIN を使った高度な検索(追加課題) +``` + +## 課題内容 + +### 前提条件 + +- データベース名: `em_oop_db` +- ユーザー: `rezouser` +- パスワード: `Rezo_0000` +- テーブル: `categories`(カテゴリ)と `products`(商品)の1対多の関係 + +### 【基本課題】CRUD処理の実装 + +#### 1. データベースの準備 +1. A5:SQL MK-2 を開く +2. データベースの追加と削除を開く +3. 追加 > MySQL / MariaDB(直接接続)を選択 +4. 以下で入力してください + +``` +ホスト名:localhost +ユーザーID:root +パスワード:Rezo_0000 +データベース:mysql +``` +5. テスト接続でOKと出ればOK +6. 「データベースを選択してください」 >> localhost/mysql を選択 +7. `start-DDL.sql` を実行してデータベースとテーブルを作成 +8. データベースの追加と削除を開く +9. 追加 > MySQL / MariaDB(直接接続)を選択 +10. 以下で入力してください + +``` +ホスト名:localhost +ユーザーID:rezouser +パスワード:Rezo_0000 +データベース:em_oop_db +``` +11. 「データベースを選択してください」 >> localhost/em_oop_db を選択 +12. `start-DML.sql` を実行して初期データを投入 + +#### 2. 段階的な実装ステップ + +以下の順番で実装を進めてください。各PHPファイルには `TODO` コメントが記載されています。 + +##### 1段階目: PDOを利用したDB接続処理を実装する +- `database/abstracts/BaseAccess.php` のコンストラクタ内でPDO接続を実装してください +- 接続情報: + - ユーザー: `rezouser` + - パスワード: `Rezo_0000` + - データベース名: `em_oop_db` + +##### 2段階目: 新しい商品を登録する +- **Create(作成)**: `public/product_register.php` - 新しい商品を登録する +- `database/products/ProductAccess.php` の `create()` メソッドを実装してください + +##### 3段階目: 既存の商品情報を更新する +- **Update(更新)**: `public/product_update.php` - 既存の商品情報を更新する +- `database/products/ProductAccess.php` の `update()` メソッドを実装してください + +##### 4段階目: 商品を削除する +- **Delete(削除)**: `public/product_delete.php` - 商品を削除する +- `database/products/ProductAccess.php` の `delete()` メソッドを実装してください + + + +### 【追加課題】JOIN を使った検索機能 + +`public/product_join.php` を実装してください。 + +この課題では、1対多の関係にある複数テーブルを結合して、より高度なデータ取得を行います。 + +#### 実装内容 + +##### 1. 商品一覧(カテゴリ情報付き)の表示 +- **INNER JOIN** を使用して、`products`テーブルと`categories`テーブルを結合 +- 取得するカラム: + - 商品の全情報(`products.*`) + - カテゴリ名(`categories.name AS category_name`) + - カテゴリ説明(`categories.description AS category_description`) +- 結合条件: `products.category_id = categories.id` +- 結果を`$productsWithCategory`配列に格納して表示 +- (参考画像 > カテゴリ情報付き商品一覧.png 参照) + +##### 2. カテゴリ別統計情報の集計 +- **LEFT JOIN** を使用して、全カテゴリの統計を取得(商品がないカテゴリも表示) +- 取得する情報: + - カテゴリID(`categories.id`) + - カテゴリ名(`categories.name`) + - 商品数(`COUNT(products.id) AS product_count`) + - 平均価格(`AVG(products.price) AS avg_price`) +- `GROUP BY categories.id, categories.name` でカテゴリごとにグループ化 +- 結果を`$categoryStats`配列に格納してカード形式で表示 +- (参考画像 > カテゴリ別統計情報.png 参照) + +#### 学習のポイント +- **INNER JOIN**: 両方のテーブルに存在するデータのみを取得 +- **LEFT JOIN**: 左側のテーブル(categories)の全レコードを取得し、右側のテーブル(products)にデータがない場合はNULLを返す +- **集計関数**: `COUNT()`で件数、`AVG()`で平均を計算 +- **GROUP BY**: データをグループ化して集計 \ No newline at end of file diff --git a/config/db_config.php b/config/db_config.php new file mode 100644 index 0000000..7b3931a --- /dev/null +++ b/config/db_config.php @@ -0,0 +1,24 @@ + PDO::ERRMODE_EXCEPTION, // エラーモードを例外に設定 + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // フェッチモードを連想配列に設定 + PDO::ATTR_EMULATE_PREPARES => false, // プリペアドステートメントのエミュレーションを無効化 +]); diff --git a/database/abstracts/BaseAccess.php b/database/abstracts/BaseAccess.php new file mode 100644 index 0000000..bc6da5f --- /dev/null +++ b/database/abstracts/BaseAccess.php @@ -0,0 +1,48 @@ +getMessage()); + } + } + + /** + * データベース接続を取得 + * + * @return PDO PDOインスタンス + */ + public function getConnection(): PDO + { + return $this->pdo; + } + + /** + * 接続を閉じる + */ + public function closeConnection(): void + { + $this->pdo = null; + } +} diff --git a/database/products/ProductAccess.php b/database/products/ProductAccess.php new file mode 100644 index 0000000..ac3c2f4 --- /dev/null +++ b/database/products/ProductAccess.php @@ -0,0 +1,151 @@ +pdo->prepare() と execute() を使用します + // 必要なカラム: category_id, name, description, price, stock + + $sql = ""; // ここにSQL文を記述 + $stmt = $this->pdo->prepare($sql); + + // ここでexecute()を実行 + + return true; + } catch (PDOException $e) { + throw new Exception("商品の登録に失敗しました: " . $e->getMessage()); + } + } + + /** + * 商品を読み取る(取得) + * + * @param int|null $id 商品ID(nullの場合は全件取得) + * @return array 商品データの配列 + * @throws Exception データベースエラーが発生した場合 + */ + public function read(?int $id = null): array + { + try { + if ($id === null) { + // 全商品を取得するSELECT文を実行 + // fetchAll()を使用して全件取得します + $sql = "SELECT * FROM products ORDER BY id"; + $stmt = $this->pdo->query($sql); + return $stmt->fetchAll(); + } else { + // 指定されたIDの商品を取得するSELECT文を実行 + // WHERE句を使用し、プリペアドステートメントで実装します + $sql = "SELECT * FROM products WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + $result = $stmt->fetch(); + return $result ? $result : []; + } + } catch (PDOException $e) { + throw new Exception("商品の取得に失敗しました: " . $e->getMessage()); + } + } + + /** + * 商品を更新する + * + * @param int $id 商品ID + * @param array $data 更新データ ['category_id', 'name', 'description', 'price', 'stock'] + * @return bool 成功した場合はtrue + * @throws Exception データベースエラーが発生した場合 + */ + public function update(int $id, array $data): bool + { + try { + // TODO: プリペアドステートメントを使用してUPDATE文を実行してください + + $sql = ""; // ここにSQL文を記述 + $stmt = $this->pdo->prepare($sql); + + // ここでexecute()を実行 + + return true; + } catch (PDOException $e) { + throw new Exception("商品の更新に失敗しました: " . $e->getMessage()); + } + } + + /** + * 商品を削除する + * + * @param int $id 商品ID + * @return bool 成功した場合はtrue + * @throws Exception データベースエラーが発生した場合 + */ + public function delete(int $id): bool + { + try { + // TODO: プリペアドステートメントを使用してDELETE文を実行してください + + $sql = ""; // ここにSQL文を記述 + $stmt = $this->pdo->prepare($sql); + + // ここでexecute()を実行 + + return true; + } catch (PDOException $e) { + throw new Exception("商品の削除に失敗しました: " . $e->getMessage()); + } + } + + /** + * カテゴリIDから商品を検索する + * + * @param int $categoryId カテゴリID + * @return array 商品データの配列 + * @throws Exception データベースエラーが発生した場合 + */ + public function findByCategoryId(int $categoryId): array + { + try { + $sql = "SELECT * FROM products WHERE category_id = :category_id ORDER BY id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['category_id' => $categoryId]); + return $stmt->fetchAll(); + } catch (PDOException $e) { + throw new Exception("商品の検索に失敗しました: " . $e->getMessage()); + } + } + + /** + * カテゴリ一覧を取得する + * + * @return array カテゴリデータの配列 + * @throws Exception データベースエラーが発生した場合 + */ + public function getCategories(): array + { + try { + $sql = "SELECT * FROM categories ORDER BY id"; + $stmt = $this->pdo->query($sql); + return $stmt->fetchAll(); + } catch (PDOException $e) { + throw new Exception("カテゴリの取得に失敗しました: " . $e->getMessage()); + } + } +} diff --git a/interfaces/DataAccessInterface.php b/interfaces/DataAccessInterface.php new file mode 100644 index 0000000..2ccb1a3 --- /dev/null +++ b/interfaces/DataAccessInterface.php @@ -0,0 +1,44 @@ + + + + + + + + 商品管理システム - PHP・DB連携演習 + + + +
+

🏪 商品管理システム

+

PHP・DB連携 総合演習課題

+ + + + + +
+

📚 学習の進め方

+

+ 1. まず「商品一覧」のコードを読んで理解しましょう
+ 2. 「商品登録」→「商品更新」→「商品削除」の順に実装しましょう
+ 3. 基本課題が完了したら「JOIN検索」に挑戦しましょう
+ 4. 各ファイルのTODOコメントを参考に実装してください +

+
+
+ + diff --git a/public/product_delete.php b/public/product_delete.php new file mode 100644 index 0000000..c53152f --- /dev/null +++ b/public/product_delete.php @@ -0,0 +1,337 @@ +read($id); + + if (empty($product)) { + $message = "指定された商品が見つかりませんでした。"; + $messageType = "error"; + } + } catch (Exception $e) { + $message = "商品の取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; + } +} + +// 全商品一覧を取得(セレクトボックス用) +$products = []; +$categories = []; +try { + $productAccess = new ProductAccess(); + $products = $productAccess->read(); + $categoriesData = $productAccess->getCategories(); + foreach ($categoriesData as $cat) { + $categories[$cat['id']] = $cat['name']; + } +} catch (Exception $e) { + $message = "データの取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; +} +?> + + + + + + + + 商品削除 - CRUD操作 + + + + +
+

🗑️ 商品削除

+

Delete - 既存の商品を削除します

+ + + + +
+ +
+ + + +
+

1. 削除する商品を選択

+
+
+ + +
+ +
+
+ + + +
+

2. 削除確認

+ +
+ ⚠️ この操作は取り消せません。本当に削除してよろしいですか? +
+ +
+

削除対象の商品情報

+
+ 商品ID: + +
+
+ カテゴリ: + +
+
+ 商品名: + +
+
+ 説明: + +
+
+ 価格: + ¥ +
+
+ 在庫数: + +
+
+ +
+ + +
+
+ +
+ + + \ No newline at end of file diff --git a/public/product_join.php b/public/product_join.php new file mode 100644 index 0000000..a0d5523 --- /dev/null +++ b/public/product_join.php @@ -0,0 +1,348 @@ +getConnection(); + + // TODO: INNER JOINを使って、商品情報とカテゴリ情報を結合して取得してください + $sql = ""; // ここにJOINを使ったSQL文を記述 + + // TODO: ここでSQLを実行して結果を$productsWithCategoryに格納 + + + // TODO: カテゴリ別の商品数と平均価格を集計してください + $statsSql = ""; // ここに集計用のSQL文を記述 + + // TODO: ここでSQLを実行して結果を$categoryStatsに格納 +} catch (Exception $e) { + $message = "データの取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; +} +?> + + + + + + + + JOIN検索 - 追加課題 + + + + +
+

🔗 JOIN を使った高度な検索

+

Advanced - 複数テーブルを結合してデータを取得します

+ + + +
+

📚 追加課題のポイント

+

+ この課題では、INNER JOIN と LEFT JOIN を使用します。
+ ・INNER JOIN: カテゴリと商品の両方に存在するデータのみを取得
+ ・LEFT JOIN: 全カテゴリを取得し、商品がない場合も表示
+ ・集計関数: COUNT()で商品数、AVG()で平均価格を計算
+ ・GROUP BY: カテゴリごとにグループ化 +

+
+ + +
+ +
+ + + +

📊 カテゴリ別統計情報

+ +
+ 統計情報を表示できません。
+ TODOを実装してください。 +
+ +
+ +
+

+
+ 商品数: + 件 +
+
+ 平均価格: + + + +
+
+ +
+ + + +

📦 商品一覧(カテゴリ情報付き)

+ +
+ 商品情報を表示できません。
+ TODOを実装してください。 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDカテゴリ商品名カテゴリ説明価格在庫登録日時
+ + + + ¥
+

+ 全 件の商品が表示されています。 +

+ +
+ + + \ No newline at end of file diff --git a/public/product_list.php b/public/product_list.php new file mode 100644 index 0000000..999abcc --- /dev/null +++ b/public/product_list.php @@ -0,0 +1,220 @@ +read(); + + // カテゴリ一覧も取得 + $categoriesData = $productAccess->getCategories(); + foreach ($categoriesData as $cat) { + $categories[$cat['id']] = $cat['name']; + } +} catch (Exception $e) { + // エラーメッセージを設定 + $message = "商品の取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; +} +?> + + + + + + + + 商品一覧 - CRUD操作 + + + + +
+

📋 商品一覧

+

Read - 登録されている商品を表示します(実装済み・参考用)

+ + + + +
+ +
+ + + +
+ 商品が登録されていません。
+ こちらから商品を登録してください。 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDカテゴリ商品名説明価格在庫登録日時
¥
+

全 件の商品が登録されています。

+ +
+ + + \ No newline at end of file diff --git a/public/product_register.php b/public/product_register.php new file mode 100644 index 0000000..cb724c3 --- /dev/null +++ b/public/product_register.php @@ -0,0 +1,247 @@ +getCategories(); +} catch (Exception $e) { + $message = "カテゴリの取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; +} +?> + + + + + + + + 商品登録 - CRUD操作 + + + + +
+

📝 商品登録

+

Create - 新しい商品を登録します

+ + + + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + + \ No newline at end of file diff --git a/public/product_update.php b/public/product_update.php new file mode 100644 index 0000000..0388f6c --- /dev/null +++ b/public/product_update.php @@ -0,0 +1,310 @@ +getMessage(); + $messageType = "error"; + } +} + +// 全商品一覧を取得(セレクトボックス用) +$products = []; +$categories = []; +try { + $productAccess = new ProductAccess(); + $products = $productAccess->read(); + $categories = $productAccess->getCategories(); +} catch (Exception $e) { + $message = "データの取得に失敗しました: " . $e->getMessage(); + $messageType = "error"; +} +?> + + + + + + + + 商品更新 - CRUD操作 + + + + +
+

✏️ 商品更新

+

Update - 既存の商品情報を更新します

+ + + + +
+ +
+ + + +
+

1. 更新する商品を選択

+
+
+ + +
+ +
+
+ + + +
+

2. 商品情報を更新

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+ + + \ No newline at end of file diff --git a/start-DDL.sql b/start-DDL.sql new file mode 100644 index 0000000..84d30c6 --- /dev/null +++ b/start-DDL.sql @@ -0,0 +1,35 @@ +-- データベースの作成 +DROP DATABASE IF EXISTS `em_oop_db`; +CREATE DATABASE `em_oop_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- ユーザ権限付与 +GRANT ALL PRIVILEGES ON `em_oop_db`.* TO 'rezouser'@'localhost'; + +-- データベースの選択 +USE `em_oop_db`; + +-- カテゴリテーブルの作成 +CREATE TABLE `categories` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `description` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 商品テーブルの作成(カテゴリとの1対多の関係) +CREATE TABLE `products` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `category_id` INT NOT NULL, + `name` VARCHAR(200) NOT NULL, + `description` TEXT, + `price` DECIMAL(10, 2) NOT NULL, + `stock` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- インデックスの作成 +CREATE INDEX `idx_category_id` ON `products`(`category_id`); +CREATE INDEX `idx_product_name` ON `products`(`name`); diff --git a/start-DML.sql b/start-DML.sql new file mode 100644 index 0000000..6b4be95 --- /dev/null +++ b/start-DML.sql @@ -0,0 +1,25 @@ +-- データベースの選択 +USE `em_oop_db`; + +-- カテゴリの初期データ投入 +INSERT INTO `categories` (`name`, `description`) VALUES +('電化製品', '家電製品やデジタル機器'), +('書籍', '本や雑誌、電子書籍'), +('食品', '生鮮食品や加工食品'), +('衣類', '洋服やアクセサリー'), +('スポーツ用品', 'スポーツ関連の商品'); + +-- 商品の初期データ投入 +INSERT INTO `products` (`category_id`, `name`, `description`, `price`, `stock`) VALUES +(1, 'ノートパソコン', '高性能なビジネス向けノートPC', 89800.00, 15), +(1, 'ワイヤレスマウス', 'Bluetooth接続の静音マウス', 2980.00, 50), +(1, 'USB充電器', '急速充電対応の充電器', 1980.00, 100), +(2, 'PHP入門書', '初心者向けのPHP解説書', 3200.00, 30), +(2, 'オブジェクト指向の本', 'OOPの基礎から応用まで', 3800.00, 25), +(2, 'データベース設計', 'DB設計のベストプラクティス', 4200.00, 20), +(3, '有機野菜セット', '新鮮な季節の野菜詰め合わせ', 1500.00, 40), +(3, '国産牛肉', 'A5ランクの黒毛和牛', 5800.00, 10), +(4, 'Tシャツ', 'コットン100%の快適なTシャツ', 2500.00, 80), +(4, 'ジーンズ', 'ストレッチ素材のデニム', 6800.00, 45), +(5, 'ランニングシューズ', '軽量で通気性の良いシューズ', 8900.00, 35), +(5, 'ヨガマット', '滑り止め付きのエクササイズマット', 3500.00, 60); diff --git a/参考画像/カテゴリ別統計情報.png b/参考画像/カテゴリ別統計情報.png new file mode 100644 index 0000000..87871cf Binary files /dev/null and b/参考画像/カテゴリ別統計情報.png differ diff --git a/参考画像/カテゴリ情報付き商品一覧.png b/参考画像/カテゴリ情報付き商品一覧.png new file mode 100644 index 0000000..9c8532a Binary files /dev/null and b/参考画像/カテゴリ情報付き商品一覧.png differ