no message
This commit is contained in:
161
Readme.md
Normal file
161
Readme.md
Normal file
@@ -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**: データをグループ化して集計
|
||||||
24
config/db_config.php
Normal file
24
config/db_config.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データベース接続設定ファイル
|
||||||
|
*
|
||||||
|
* このファイルでは、データベースへの接続情報を定義しています。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// データベース接続情報
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'em_oop_db');
|
||||||
|
define('DB_USER', 'rezouser');
|
||||||
|
define('DB_PASS', 'Rezo_0000');
|
||||||
|
define('DB_CHARSET', 'utf8mb4');
|
||||||
|
|
||||||
|
// DSN(Data Source Name)の構築
|
||||||
|
define('DB_DSN', 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET);
|
||||||
|
|
||||||
|
// PDOのオプション設定
|
||||||
|
define('DB_OPTIONS', [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // エラーモードを例外に設定
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // フェッチモードを連想配列に設定
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false, // プリペアドステートメントのエミュレーションを無効化
|
||||||
|
]);
|
||||||
48
database/abstracts/BaseAccess.php
Normal file
48
database/abstracts/BaseAccess.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../config/db_config.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BaseAccessクラス(抽象クラス)
|
||||||
|
*
|
||||||
|
* データベースアクセスの基底クラスです。
|
||||||
|
* 共通のデータベース接続機能を提供します。
|
||||||
|
*/
|
||||||
|
abstract class BaseAccess
|
||||||
|
{
|
||||||
|
protected ?PDO $pdo = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* コンストラクタ
|
||||||
|
* データベース接続を確立します。
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// TODO: PDOを使用してデータベースに接続してください
|
||||||
|
// ヒント: new PDO(DSN, ユーザー名, パスワード, オプション)
|
||||||
|
// 接続情報は config/db_config.php で定義されています
|
||||||
|
// ユーザー: rezouser
|
||||||
|
// パスワード: Rezo_0000
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new Exception("データベース接続エラー: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データベース接続を取得
|
||||||
|
*
|
||||||
|
* @return PDO PDOインスタンス
|
||||||
|
*/
|
||||||
|
public function getConnection(): PDO
|
||||||
|
{
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接続を閉じる
|
||||||
|
*/
|
||||||
|
public function closeConnection(): void
|
||||||
|
{
|
||||||
|
$this->pdo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
database/products/ProductAccess.php
Normal file
151
database/products/ProductAccess.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../abstracts/BaseAccess.php';
|
||||||
|
require_once __DIR__ . '/../../interfaces/DataAccessInterface.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProductAccessクラス
|
||||||
|
*
|
||||||
|
* DataAccessInterfaceを実装し、商品データの永続化を担当します。
|
||||||
|
* BaseAccessを継承してデータベース接続機能を利用します。
|
||||||
|
* インタフェースを通じた多態性(ポリモーフィズム)を実現しています。
|
||||||
|
*/
|
||||||
|
class ProductAccess extends BaseAccess implements DataAccessInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 商品を作成(登録)する
|
||||||
|
*
|
||||||
|
* @param array $data 商品データ ['category_id', 'name', 'description', 'price', 'stock']
|
||||||
|
* @return bool 成功した場合はtrue
|
||||||
|
* @throws Exception データベースエラーが発生した場合
|
||||||
|
*/
|
||||||
|
public function create(array $data): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// TODO: プリペアドステートメントを使用してINSERT文を実行してください
|
||||||
|
// ヒント: $this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
interfaces/DataAccessInterface.php
Normal file
44
interfaces/DataAccessInterface.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データアクセスインタフェース
|
||||||
|
*
|
||||||
|
* このインタフェースを実装することで、
|
||||||
|
* 基本的なデータベース操作(CRUD)の統一的な実装を保証します。
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DataAccessInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* データを作成(登録)する
|
||||||
|
*
|
||||||
|
* @param array $data 登録するデータの配列
|
||||||
|
* @return bool 成功した場合はtrue、失敗した場合はfalse
|
||||||
|
*/
|
||||||
|
public function create(array $data): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データを読み取る(取得)
|
||||||
|
*
|
||||||
|
* @param int|null $id 取得するデータのID(nullの場合は全件取得)
|
||||||
|
* @return array 取得したデータの配列
|
||||||
|
*/
|
||||||
|
public function read(?int $id = null): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データを更新する
|
||||||
|
*
|
||||||
|
* @param int $id 更新するデータのID
|
||||||
|
* @param array $data 更新するデータの配列
|
||||||
|
* @return bool 成功した場合はtrue、失敗した場合はfalse
|
||||||
|
*/
|
||||||
|
public function update(int $id, array $data): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* データを削除する
|
||||||
|
*
|
||||||
|
* @param int $id 削除するデータのID
|
||||||
|
* @return bool 成功した場合はtrue、失敗した場合はfalse
|
||||||
|
*/
|
||||||
|
public function delete(int $id): bool;
|
||||||
|
}
|
||||||
167
public/index.php
Normal file
167
public/index.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* トップページ
|
||||||
|
*
|
||||||
|
* 商品管理システムのメインページです。
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>商品管理システム - PHP・DB連携演習</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.menu-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.menu-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.menu-card .icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.menu-card h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.menu-card p {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.advanced-section {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 2px solid #eee;
|
||||||
|
}
|
||||||
|
.advanced-section h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.advanced-card {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.info-box h3 {
|
||||||
|
color: #1976D2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.info-box p {
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏪 商品管理システム</h1>
|
||||||
|
<p class="subtitle">PHP・DB連携 総合演習課題</p>
|
||||||
|
|
||||||
|
<div class="menu-grid">
|
||||||
|
<a href="product_list.php" class="menu-card">
|
||||||
|
<div class="icon">📋</div>
|
||||||
|
<h2>商品一覧</h2>
|
||||||
|
<p>登録されている商品を表示<br>(実装済み・参考用)</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="product_register.php" class="menu-card">
|
||||||
|
<div class="icon">📝</div>
|
||||||
|
<h2>商品登録</h2>
|
||||||
|
<p>新しい商品を登録する<br>(ステップ2)</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="product_update.php" class="menu-card">
|
||||||
|
<div class="icon">✏️</div>
|
||||||
|
<h2>商品更新</h2>
|
||||||
|
<p>既存の商品情報を更新<br>(ステップ3)</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="product_delete.php" class="menu-card">
|
||||||
|
<div class="icon">🗑️</div>
|
||||||
|
<h2>商品削除</h2>
|
||||||
|
<p>商品を削除する<br>(ステップ4)</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="advanced-section">
|
||||||
|
<h3>🎓 追加課題</h3>
|
||||||
|
<div class="menu-grid">
|
||||||
|
<a href="product_join.php" class="menu-card advanced-card">
|
||||||
|
<div class="icon">🔗</div>
|
||||||
|
<h2>JOIN検索</h2>
|
||||||
|
<p>複数テーブルを結合した<br>高度な検索機能</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>📚 学習の進め方</h3>
|
||||||
|
<p>
|
||||||
|
<strong>1.</strong> まず「商品一覧」のコードを読んで理解しましょう<br>
|
||||||
|
<strong>2.</strong> 「商品登録」→「商品更新」→「商品削除」の順に実装しましょう<br>
|
||||||
|
<strong>3.</strong> 基本課題が完了したら「JOIN検索」に挑戦しましょう<br>
|
||||||
|
<strong>4.</strong> 各ファイルのTODOコメントを参考に実装してください
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
337
public/product_delete.php
Normal file
337
public/product_delete.php
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../database/products/ProductAccess.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品削除処理(Delete)
|
||||||
|
*
|
||||||
|
* このファイルでは、既存の商品をデータベースから削除します。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// エラー表示設定(開発時のみ)
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
$product = null;
|
||||||
|
|
||||||
|
// POSTリクエストの処理(削除実行)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) {
|
||||||
|
try {
|
||||||
|
$id = (int)$_POST['id'];
|
||||||
|
|
||||||
|
// TODO: ProductAccessのインスタンスを作成してください
|
||||||
|
$productAccess = null; // ここを実装
|
||||||
|
|
||||||
|
// TODO: productAccessのdelete()メソッドを呼び出して商品を削除してください
|
||||||
|
|
||||||
|
$message = "商品を削除しました!";
|
||||||
|
$messageType = "success";
|
||||||
|
$product = null; // 削除後は商品情報をクリア
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// TODO: エラーメッセージを$messageに設定してください
|
||||||
|
$message = "";
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETリクエストの処理(商品情報取得)
|
||||||
|
if (isset($_GET['id']) && empty($_POST)) {
|
||||||
|
try {
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
$productAccess = new ProductAccess();
|
||||||
|
$product = $productAccess->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";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>商品削除 - CRUD操作</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: #667eea;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 120px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🗑️ 商品削除</h1>
|
||||||
|
<p class="subtitle">Delete - 既存の商品を削除します</p>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.php">🏠 トップ</a>
|
||||||
|
<a href="product_list.php">📋 一覧表示</a>
|
||||||
|
<a href="product_register.php">📝 新規登録</a>
|
||||||
|
<a href="product_update.php">✏️ 更新</a>
|
||||||
|
<a href="product_join.php">🔗 JOIN検索</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="message <?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 商品選択セクション -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>1. 削除する商品を選択</h2>
|
||||||
|
<form method="GET" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product_select">商品選択</label>
|
||||||
|
<select name="id" id="product_select">
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<?php foreach ($products as $p): ?>
|
||||||
|
<option value="<?php echo $p['id']; ?>"
|
||||||
|
<?php echo (isset($_GET['id']) && $_GET['id'] == $p['id']) ? 'selected' : ''; ?>>
|
||||||
|
[ID: <?php echo $p['id']; ?>] <?php echo htmlspecialchars($p['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-search">商品情報を取得</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 削除確認セクション -->
|
||||||
|
<?php if ($product): ?>
|
||||||
|
<div class="section">
|
||||||
|
<h2>2. 削除確認</h2>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
⚠️ この操作は取り消せません。本当に削除してよろしいですか?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>削除対象の商品情報</h3>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">商品ID:</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($product['id']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">カテゴリ:</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($categories[$product['category_id']] ?? '不明'); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">商品名:</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($product['name']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">説明:</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($product['description'] ?? '-'); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">価格:</span>
|
||||||
|
<span class="info-value">¥<?php echo number_format($product['price']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">在庫数:</span>
|
||||||
|
<span class="info-value"><?php echo htmlspecialchars($product['stock']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="" onsubmit="return confirm('本当に削除しますか?');">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $product['id']; ?>">
|
||||||
|
<button type="submit" name="delete" class="btn btn-danger">削除する</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
348
public/product_join.php
Normal file
348
public/product_join.php
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../database/products/ProductAccess.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JOIN を使った高度な検索(追加課題)
|
||||||
|
*
|
||||||
|
* このファイルでは、categoriesテーブルとproductsテーブルを結合して、
|
||||||
|
* カテゴリ情報と共に商品情報を取得します。
|
||||||
|
* また、カテゴリ別の商品数を集計する機能も実装します。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// エラー表示設定(開発時のみ)
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
$productsWithCategory = [];
|
||||||
|
$categoryStats = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ProductAccessを使ってPDO接続を取得
|
||||||
|
$productAccess = new ProductAccess();
|
||||||
|
$pdo = $productAccess->getConnection();
|
||||||
|
|
||||||
|
// TODO: INNER JOINを使って、商品情報とカテゴリ情報を結合して取得してください
|
||||||
|
$sql = ""; // ここにJOINを使ったSQL文を記述
|
||||||
|
|
||||||
|
// TODO: ここでSQLを実行して結果を$productsWithCategoryに格納
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: カテゴリ別の商品数と平均価格を集計してください
|
||||||
|
$statsSql = ""; // ここに集計用のSQL文を記述
|
||||||
|
|
||||||
|
// TODO: ここでSQLを実行して結果を$categoryStatsに格納
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$message = "データの取得に失敗しました: " . $e->getMessage();
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JOIN検索 - 追加課題</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #555;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 22px;
|
||||||
|
border-bottom: 2px solid #11998e;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #11998e;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: #0e7d72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #11998e;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #11998e;
|
||||||
|
color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box h3 {
|
||||||
|
color: #1976D2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔗 JOIN を使った高度な検索</h1>
|
||||||
|
<p class="subtitle">Advanced - 複数テーブルを結合してデータを取得します</p>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.php">🏠 トップ</a>
|
||||||
|
<a href="product_list.php">📋 一覧表示</a>
|
||||||
|
<a href="product_register.php">📝 新規登録</a>
|
||||||
|
<a href="product_update.php">✏️ 更新</a>
|
||||||
|
<a href="product_delete.php">🗑️ 削除</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>📚 追加課題のポイント</h3>
|
||||||
|
<p>
|
||||||
|
この課題では、<strong>INNER JOIN</strong> と <strong>LEFT JOIN</strong> を使用します。<br>
|
||||||
|
・INNER JOIN: カテゴリと商品の両方に存在するデータのみを取得<br>
|
||||||
|
・LEFT JOIN: 全カテゴリを取得し、商品がない場合も表示<br>
|
||||||
|
・集計関数: COUNT()で商品数、AVG()で平均価格を計算<br>
|
||||||
|
・GROUP BY: カテゴリごとにグループ化
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="message <?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- カテゴリ別統計情報 -->
|
||||||
|
<h2>📊 カテゴリ別統計情報</h2>
|
||||||
|
<?php if (empty($categoryStats)): ?>
|
||||||
|
<div class="no-data">
|
||||||
|
統計情報を表示できません。<br>
|
||||||
|
TODOを実装してください。
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<?php foreach ($categoryStats as $stat): ?>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3><?php echo htmlspecialchars($stat['name']); ?></h3>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">商品数:</span>
|
||||||
|
<span class="stat-value"><?php echo $stat['product_count']; ?> 件</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">平均価格:</span>
|
||||||
|
<span class="stat-value">
|
||||||
|
<?php
|
||||||
|
echo $stat['avg_price']
|
||||||
|
? '¥' . number_format($stat['avg_price'], 0)
|
||||||
|
: '商品なし';
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 商品一覧(カテゴリ情報付き) -->
|
||||||
|
<h2>📦 商品一覧(カテゴリ情報付き)</h2>
|
||||||
|
<?php if (empty($productsWithCategory)): ?>
|
||||||
|
<div class="no-data">
|
||||||
|
商品情報を表示できません。<br>
|
||||||
|
TODOを実装してください。
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>カテゴリ</th>
|
||||||
|
<th>商品名</th>
|
||||||
|
<th>カテゴリ説明</th>
|
||||||
|
<th>価格</th>
|
||||||
|
<th>在庫</th>
|
||||||
|
<th>登録日時</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($productsWithCategory as $product): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($product['id']); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="category-badge">
|
||||||
|
<?php echo htmlspecialchars($product['category_name']); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['category_description'] ?? '-'); ?></td>
|
||||||
|
<td class="price">¥<?php echo number_format($product['price']); ?></td>
|
||||||
|
<td class="stock"><?php echo htmlspecialchars($product['stock']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['created_at']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="margin-top: 15px; color: #666; font-size: 14px;">
|
||||||
|
全 <?php echo count($productsWithCategory); ?> 件の商品が表示されています。
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
220
public/product_list.php
Normal file
220
public/product_list.php
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../database/products/ProductAccess.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品一覧表示処理(Read)
|
||||||
|
*
|
||||||
|
* このファイルでは、登録されている全商品をデータベースから取得して表示します。
|
||||||
|
* このファイルは実装済みです。他のファイルを実装する際の参考にしてください。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// エラー表示設定(開発時のみ)
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
$categories = [];
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ProductAccessのインスタンスを作成
|
||||||
|
$productAccess = new ProductAccess();
|
||||||
|
|
||||||
|
// read()メソッドを呼び出して全商品を取得
|
||||||
|
// 引数を渡さない(またはnullを渡す)と全件取得できます
|
||||||
|
$products = $productAccess->read();
|
||||||
|
|
||||||
|
// カテゴリ一覧も取得
|
||||||
|
$categoriesData = $productAccess->getCategories();
|
||||||
|
foreach ($categoriesData as $cat) {
|
||||||
|
$categories[$cat['id']] = $cat['name'];
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// エラーメッセージを設定
|
||||||
|
$message = "商品の取得に失敗しました: " . $e->getMessage();
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>商品一覧 - CRUD操作</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📋 商品一覧</h1>
|
||||||
|
<p class="subtitle">Read - 登録されている商品を表示します(実装済み・参考用)</p>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.php">🏠 トップ</a>
|
||||||
|
<a href="product_register.php">📝 新規登録</a>
|
||||||
|
<a href="product_update.php">✏️ 更新</a>
|
||||||
|
<a href="product_delete.php">🗑️ 削除</a>
|
||||||
|
<a href="product_join.php">🔗 JOIN検索</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="message <?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (empty($products)): ?>
|
||||||
|
<div class="no-data">
|
||||||
|
商品が登録されていません。<br>
|
||||||
|
<a href="product_register.php">こちら</a>から商品を登録してください。
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>カテゴリ</th>
|
||||||
|
<th>商品名</th>
|
||||||
|
<th>説明</th>
|
||||||
|
<th>価格</th>
|
||||||
|
<th>在庫</th>
|
||||||
|
<th>登録日時</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($products as $product): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($product['id']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($categories[$product['category_id']] ?? '不明'); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['description'] ?? '-'); ?></td>
|
||||||
|
<td class="price">¥<?php echo number_format($product['price']); ?></td>
|
||||||
|
<td class="stock"><?php echo htmlspecialchars($product['stock']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['created_at']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="count">全 <?php echo count($products); ?> 件の商品が登録されています。</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
247
public/product_register.php
Normal file
247
public/product_register.php
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../database/products/ProductAccess.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品登録処理(Create)
|
||||||
|
*
|
||||||
|
* このファイルでは、新しい商品をデータベースに登録します。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// エラー表示設定(開発時のみ)
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
|
||||||
|
// POSTリクエストの処理
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
// TODO: ProductAccessのインスタンスを作成してください
|
||||||
|
$productAccess = null; // ここを実装
|
||||||
|
|
||||||
|
// TODO: POSTデータから商品情報を配列にまとめてください
|
||||||
|
// 必要な情報: category_id, name, description, price, stock
|
||||||
|
$productData = [
|
||||||
|
// ここにPOSTデータを設定
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: productAccessのcreate()メソッドを呼び出して商品を登録してください
|
||||||
|
|
||||||
|
|
||||||
|
// 成功メッセージ
|
||||||
|
$message = "商品を登録しました!";
|
||||||
|
$messageType = "success";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// TODO: エラーメッセージを$messageに設定してください
|
||||||
|
$message = "";
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// カテゴリ一覧を取得(セレクトボックス用)
|
||||||
|
$categories = [];
|
||||||
|
try {
|
||||||
|
$productAccess = new ProductAccess();
|
||||||
|
$categories = $productAccess->getCategories();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$message = "カテゴリの取得に失敗しました: " . $e->getMessage();
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>商品登録 - CRUD操作</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📝 商品登録</h1>
|
||||||
|
<p class="subtitle">Create - 新しい商品を登録します</p>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.php">🏠 トップ</a>
|
||||||
|
<a href="product_list.php">📋 一覧表示</a>
|
||||||
|
<a href="product_update.php">✏️ 更新</a>
|
||||||
|
<a href="product_delete.php">🗑️ 削除</a>
|
||||||
|
<a href="product_join.php">🔗 JOIN検索</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="message <?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="product_register.php">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">カテゴリ *</label>
|
||||||
|
<select name="category_id" id="category_id" required>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<?php foreach ($categories as $category): ?>
|
||||||
|
<option value="<?php echo $category['id']; ?>">
|
||||||
|
<?php echo htmlspecialchars($category['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">商品名 *</label>
|
||||||
|
<input type="text" name="name" id="name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">説明</label>
|
||||||
|
<textarea name="description" id="description"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price">価格 *</label>
|
||||||
|
<input type="number" name="price" id="price" step="0.01" min="0" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock">在庫数 *</label>
|
||||||
|
<input type="number" name="stock" id="stock" min="0" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">登録する</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
310
public/product_update.php
Normal file
310
public/product_update.php
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../database/products/ProductAccess.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品更新処理(Update)
|
||||||
|
*
|
||||||
|
* このファイルでは、既存の商品情報を更新します。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// エラー表示設定(開発時のみ)
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
$product = null;
|
||||||
|
|
||||||
|
// POSTリクエストの処理(更新実行)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update'])) {
|
||||||
|
try {
|
||||||
|
$id = (int)$_POST['id'];
|
||||||
|
|
||||||
|
// TODO: ProductAccessのインスタンスを作成してください
|
||||||
|
$productAccess = null; // ここを実装
|
||||||
|
|
||||||
|
// TODO: POSTデータから商品情報を配列にまとめてください
|
||||||
|
$productData = [
|
||||||
|
// ここにPOSTデータを設定
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: productAccessのupdate()メソッドを呼び出して商品を更新してください
|
||||||
|
|
||||||
|
|
||||||
|
$message = "商品情報を更新しました!";
|
||||||
|
$messageType = "success";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// TODO: エラーメッセージを$messageに設定してください
|
||||||
|
$message = "";
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETリクエストの処理(商品情報取得)
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
try {
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
|
||||||
|
// TODO: ProductAccessのインスタンスを作成してください
|
||||||
|
$productAccess = null; // ここを実装
|
||||||
|
|
||||||
|
// TODO: productAccessのread()メソッドを呼び出して商品情報を取得してください
|
||||||
|
$product = null; // ここを実装
|
||||||
|
|
||||||
|
if (empty($product)) {
|
||||||
|
$message = "指定された商品が見つかりませんでした。";
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$message = "商品の取得に失敗しました: " . $e->getMessage();
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全商品一覧を取得(セレクトボックス用)
|
||||||
|
$products = [];
|
||||||
|
$categories = [];
|
||||||
|
try {
|
||||||
|
$productAccess = new ProductAccess();
|
||||||
|
$products = $productAccess->read();
|
||||||
|
$categories = $productAccess->getCategories();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$message = "データの取得に失敗しました: " . $e->getMessage();
|
||||||
|
$messageType = "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>商品更新 - CRUD操作</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>✏️ 商品更新</h1>
|
||||||
|
<p class="subtitle">Update - 既存の商品情報を更新します</p>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.php">🏠 トップ</a>
|
||||||
|
<a href="product_list.php">📋 一覧表示</a>
|
||||||
|
<a href="product_register.php">📝 新規登録</a>
|
||||||
|
<a href="product_delete.php">🗑️ 削除</a>
|
||||||
|
<a href="product_join.php">🔗 JOIN検索</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="message <?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 商品選択セクション -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>1. 更新する商品を選択</h2>
|
||||||
|
<form method="GET" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product_select">商品選択</label>
|
||||||
|
<select name="id" id="product_select">
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<?php foreach ($products as $p): ?>
|
||||||
|
<option value="<?php echo $p['id']; ?>"
|
||||||
|
<?php echo (isset($_GET['id']) && $_GET['id'] == $p['id']) ? 'selected' : ''; ?>>
|
||||||
|
[ID: <?php echo $p['id']; ?>] <?php echo htmlspecialchars($p['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-search">商品情報を取得</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新フォームセクション -->
|
||||||
|
<?php if ($product): ?>
|
||||||
|
<div class="section">
|
||||||
|
<h2>2. 商品情報を更新</h2>
|
||||||
|
<form method="POST" action="">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $product['id']; ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">カテゴリ *</label>
|
||||||
|
<select name="category_id" id="category_id" required>
|
||||||
|
<?php foreach ($categories as $category): ?>
|
||||||
|
<option value="<?php echo $category['id']; ?>"
|
||||||
|
<?php echo ($product['category_id'] == $category['id']) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($category['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">商品名 *</label>
|
||||||
|
<input type="text" name="name" id="name"
|
||||||
|
value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">説明</label>
|
||||||
|
<textarea name="description" id="description"><?php echo htmlspecialchars($product['description'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price">価格 *</label>
|
||||||
|
<input type="number" name="price" id="price" step="0.01" min="0"
|
||||||
|
value="<?php echo $product['price']; ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock">在庫数 *</label>
|
||||||
|
<input type="number" name="stock" id="stock" min="0"
|
||||||
|
value="<?php echo $product['stock']; ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" name="update" class="btn">更新する</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
35
start-DDL.sql
Normal file
35
start-DDL.sql
Normal file
@@ -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`);
|
||||||
25
start-DML.sql
Normal file
25
start-DML.sql
Normal file
@@ -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);
|
||||||
BIN
参考画像/カテゴリ別統計情報.png
Normal file
BIN
参考画像/カテゴリ別統計情報.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
BIN
参考画像/カテゴリ情報付き商品一覧.png
Normal file
BIN
参考画像/カテゴリ情報付き商品一覧.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Reference in New Issue
Block a user