commit f81638a7b3ed486a2fbb65454580ee73163a443b Author: a-kizaki Date: Thu Oct 22 08:48:13 2026 +0900 no message diff --git a/EmployeeData.csv b/EmployeeData.csv new file mode 100644 index 0000000..3ecdf53 --- /dev/null +++ b/EmployeeData.csv @@ -0,0 +1,4 @@ +1610000001,"山田太郎",yamada@gmail.com +1610000002,"鈴木花子",suzuki@gmail.com +1610000003,"麻枝史明",maeda@gmail.com +1610000004,"丸田章介",maruda@gmail.com diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..5c163f3 --- /dev/null +++ b/Readme.md @@ -0,0 +1,102 @@ +# 社員メンテナンスシステム 演習課題 + +## はじめに(演習の⽬的と取り組み⽅) +当演習は、講義で学んだことを⾃⼒で実践できるようになるために実施します。 そのために以下の内容を守って演習に取り組んでください。 + +### 納品物、納期を守りましょう +この演習は、実際の仕事を想定した内容です。 +提出物の内容と提出期限は必ず守ってください。 +作業が間に合わない、提出物に問題が出そうな場合は、**必ずルールにしたがって適切な相⼿に相談してください**。 + +### 適切な⼿順で開発しましょう +プログラムを書き始める前に、**README.md** や**仕様書**(配布されている場合) を必ず読んでください。 +「**何を作るのか**」、「**どこまで作ればよいのか**」を理解してから作業を始めてください。 + +### 知らないことは⾃分で調べましょう +この演習には、**講義で扱っていない内容が⼀部含まれています**。 +その場合は、インターネットなどで⾃分で調べながら進めてください。 +調べることも、エンジニアにとって⼤切なスキルの⼀つです。 + + +## 演習目的 +**継承** +- 共通処理(CSVの読み書き)は、BaseAccess という抽象クラスにまとめています。 + +**インタフェース** +- DataAccessInterface によって、EmployeeAccess クラスは必ず getAll(), add(), update(), delete() を実装することが保証されます。 + +**カプセル化** +- バリデーションロジック(`validateEmployeeData()`)やデータ変換ロジック(`convertRowToEmployee()`)をprivateメソッドとして隠蔽し、外部から直接アクセスできないようにしています。 +- プロパティ(`$file`)はprotectedで定義し、継承クラスからのみアクセス可能にしています。 +- これにより、クラスの内部実装の詳細を隠し、外部からは公開されたメソッドのみを使用する設計になっています。 + +**例外処理** +- ファイル操作やデータ検証でエラーが発生した場合、適切な例外(`InvalidArgumentException`、`RuntimeException`)をスローします。 +- controller.phpでは、これらの例外をキャッチしてユーザーに分かりやすいエラーメッセージを表示します。 + +これにより、同じ処理を複数のクラスに記述せずに再利用できる設計のメリットと、 +将来的に DB 連携版や部署 CRUD への拡張が容易になる点を体感してもらいます。 + + +## フォルダ構成 +project/ +├─ database/ +│ ├─ abstracts/ +│ │ └─ BaseAccess.php // CSV共通処理の抽象クラス +│ ├─ interfaces/ +│ │ └─ DataAccessInterface.php // CRUD操作のインタフェース +│ |─ employees/ +│ | └─ EmployeeAccess.php // 社員のCSV操作を実装したクラス +├─ EmployeeData.csv // 社員情報のCSVファイル +│─ public/ +│ ├─ controller.php // 各画面からの指示を処理するファイル +│ ├─ employee_list.php // 社員一覧表示画面 +│ ├─ employee_register.php // 社員登録画面 +│ ├─ employee_update.php // 社員更新画面 +│ └─ index.php // ホーム画面(各機能へのリンクあり) + + +## 課題内容 +**database/employees/EmployeeAccess.php** の TODOコメントに従って、各メソッド(add, update, delete)を完成させてください。 + +- **getAll() メソッドは実装済み**なので、参考にしてください。 +- TODOコメントが実装のヒントになっています。 +- 各メソッドの詳細な仕様は後述の「各メソッド説明」を確認してください。 + +### 利用可能なメソッド +- `$this->readData()`: CSVから全データを読み込む(BaseAccessから継承) +- `$this->writeData($data)`: CSVにデータを書き込む(BaseAccessから継承) +- `$this->generateId($rows)`: 新しいIDを生成(privateメソッド) +- `$this->validateEmployeeData($data)`: データバリデーション(privateメソッド) +- `$this->convertRowToEmployee($row)`: CSV行を連想配列に変換(privateメソッド) + +**困ったときは `ヒント.txt` を参照してください。** + + +## 各メソッド説明 +- **getAll()** (実装済み) + 全社員情報を取得して連想配列の配列で返す。 + +- **add($data)** + 新規社員を登録する。IDは自動生成。 + $data = ['name' => '田中五郎', 'email' => 'tanaka@example.com'] + +- **update($id, $data)** + 指定IDの社員情報を更新する。 + IDが存在しない場合は `RuntimeException` をスロー。 + +- **delete($id)** + 指定IDの社員を削除する。 + IDが存在しない場合は `RuntimeException` をスロー。 + + + +## カプセル化のポイント +- **privateメソッド**: `validateEmployeeData()`, `convertRowToEmployee()`, `generateId()` は内部処理のためprivateで定義されています。 +- **protectedプロパティ**: `$file` は継承クラスからのみアクセス可能なprotectedで定義されています。 +- **publicメソッド**: 外部から呼び出す必要があるメソッドのみがpublicで定義されています。 + +## 例外処理のポイント +- **InvalidArgumentException**: 入力データが不正な場合(必須項目が空、メールアドレス形式が不正など) +- **RuntimeException**: ファイル操作やデータ操作に失敗した場合(ファイルが開けない、IDが見つからないなど) +- controller.phpでは、これらの例外をキャッチしてユーザーに分かりやすいエラーメッセージを表示します。 diff --git a/database/abstracts/BaseAccess.php b/database/abstracts/BaseAccess.php new file mode 100644 index 0000000..bc22a28 --- /dev/null +++ b/database/abstracts/BaseAccess.php @@ -0,0 +1,103 @@ +file = $file; + } + + /** + * CSVファイルから全データを読み込む + * カプセル化: protectedで外部から直接呼び出せない + * @return array CSVデータの配列 + * @throws RuntimeException ファイル読み込みに失敗した場合 + */ + protected function readData() + { + if (!file_exists($this->file)) { + return array(); + } + + $handle = fopen($this->file, "r"); + if ($handle === false) { + throw new RuntimeException("ファイルを開けませんでした: " . $this->file); + } + + try { + $data = array(); + while ($row = fgetcsv($handle)) { + // 空行や必要列数が足りない場合はスキップ + if (empty($row) || count($row) < 3) { + continue; + } + $data[] = $row; + } + return $data; + } finally { + fclose($handle); + } + } + + /** + * CSVファイルにデータを書き込む + * カプセル化: protectedで外部から直接呼び出せない + * @param array $data 書き込むデータの配列 + * @return bool 書き込み成功時true + * @throws RuntimeException ファイル書き込みに失敗した場合 + */ + protected function writeData($data) + { + if (!is_array($data)) { + throw new InvalidArgumentException("書き込むデータは配列である必要があります"); + } + + // ディレクトリが存在しない場合は作成 + $directory = dirname($this->file); + if (!is_dir($directory)) { + if (!mkdir($directory, 0755, true)) { + throw new RuntimeException("ディレクトリを作成できませんでした: " . $directory); + } + } + + $handle = fopen($this->file, "w"); + if ($handle === false) { + throw new RuntimeException("ファイルに書き込めませんでした: " . $this->file); + } + + try { + foreach ($data as $row) { + if (fputcsv($handle, $row) === false) { + throw new RuntimeException("CSVデータの書き込みに失敗しました"); + } + } + return true; + } finally { + fclose($handle); + } + } + + /** + * ファイルパスを取得(カプセル化: 読み取り専用アクセサ) + * @return string ファイルパス + */ + public function getFilePath() + { + return $this->file; + } +} diff --git a/database/employees/EmployeeAccess.php b/database/employees/EmployeeAccess.php new file mode 100644 index 0000000..5ba5863 --- /dev/null +++ b/database/employees/EmployeeAccess.php @@ -0,0 +1,174 @@ +readData(); + $employees = array(); + + // 各行のデータを 'id', 'name', 'email' の連想配列に変換する + foreach ($rows as $row) { + $employees[] = $this->convertRowToEmployee($row); + } + + return $employees; + } catch (Exception $e) { + throw new RuntimeException("社員情報の取得に失敗しました: " . $e->getMessage(), 0, $e); + } + } + + /** + * 新規登録(INSERT) + * @param array $data ['name' => '...', 'email' => '...'] の連想配列 + * @return bool 登録成功時true + * @throws InvalidArgumentException データが不正な場合 + * @throws RuntimeException データ書き込みに失敗した場合 + */ + public function add($data) + { + // カプセル化: バリデーションロジックをprivateメソッドで隠蔽 + $this->validateEmployeeData($data); + + try { + // TODO:既存データを取得 + + // TODO:新しいIDを生成 + + // TODO:CSVファイルに新規データを追加する処理 + + // CSVファイルに書き込み + return $this->writeData(); + } catch (Exception $e) { + throw new RuntimeException("社員情報の登録に失敗しました: " . $e->getMessage(), 0, $e); + } + } + + /** + * 更新(UPDATE) + * @param string|int $id 更新する社員のID + * @param array $data ['name' => '...', 'email' => '...'] の連想配列 + * @return bool 更新成功時true + * @throws InvalidArgumentException データが不正な場合 + * @throws RuntimeException データ書き込みに失敗した場合、またはIDが見つからない場合 + */ + public function update($id, $data) + { + // カプセル化: バリデーションロジックをprivateメソッドで隠蔽 + $this->validateEmployeeData($data); + + // IDが指定されていない場合はエラー + if (empty($id)) { + throw new InvalidArgumentException("IDが指定されていません"); + } + + try { + // CSVファイルから全データを取得 + $rows = $this->readData(); + + // 指定されたIDの社員が見つかったかをフラグで管理 + $found = false; + + // TODO: foreachを使い、$rows の各行を参照渡しでチェックして、$id と一致する行の 'name' と 'email' を更新する + // TODO: 見つかったらフラグを立てる + // TODO: 見つからなければエラー + + + + + // 見つからなければエラー + if (!$found) { + throw new RuntimeException("指定されたIDの社員が見つかりません: " . $id); + } + + // CSVファイルに書き込み + return $this->writeData($rows); + } catch (RuntimeException $e) { + throw $e; + } catch (Exception $e) { + throw new RuntimeException("社員情報の更新に失敗しました: " . $e->getMessage(), 0, $e); + } + } + + /** + * 削除(DELETE) + * @param string|int $id 削除する社員のID + * @return bool 削除成功時true + * @throws InvalidArgumentException IDが指定されていない場合 + * @throws RuntimeException データ書き込みに失敗した場合、またはIDが見つからない場合 + */ + public function delete($id) + { + // TODO: ゼロから実装しましょう + } + + /** + * CSV行データを社員情報の連想配列に変換する(カプセル化: privateメソッド) + * @param array $row CSVの1行データ + * @return array ['id' => ..., 'name' => ..., 'email' => ...] の連想配列 + */ + private function convertRowToEmployee($row) + { + return array( + 'id' => isset($row[0]) ? $row[0] : '', + 'name' => isset($row[1]) ? $row[1] : '', + 'email' => isset($row[2]) ? $row[2] : '' + ); + } + + /** + * 社員データのバリデーション(カプセル化: privateメソッド) + * @param array $data バリデーション対象のデータ + * @throws InvalidArgumentException データが不正な場合 + */ + private function validateEmployeeData($data) + { + if (!is_array($data)) { + throw new InvalidArgumentException("データは配列である必要があります"); + } + + if (!isset($data['name']) || empty(trim($data['name']))) { + throw new InvalidArgumentException("名前が指定されていません"); + } + + if (!isset($data['email']) || empty(trim($data['email']))) { + throw new InvalidArgumentException("メールアドレスが指定されていません"); + } + + // メールアドレスの形式チェック + if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException("メールアドレスの形式が正しくありません"); + } + } + + /** + * 新しいIDを生成する(カプセル化: privateメソッド) + * @param array $rows 既存のデータ行 + * @return string 生成されたID + */ + private function generateId($rows) + { + // 既存のIDの最大値を取得して+1する方式 + $maxId = 0; + foreach ($rows as $row) { + if (isset($row[0]) && is_numeric($row[0])) { + $maxId = max($maxId, (int)$row[0]); + } + } + return (string)($maxId + 1); + } +} diff --git a/database/interfaces/DataAccessInterface.php b/database/interfaces/DataAccessInterface.php new file mode 100644 index 0000000..aec69a6 --- /dev/null +++ b/database/interfaces/DataAccessInterface.php @@ -0,0 +1,8 @@ +getAll(); + $employees = array(); + if ($keyword != "") { + foreach ($allEmployees as $emp) { + if (strpos($emp['name'], $keyword) !== false) { + $employees[] = $emp; + } + } + } else { + $employees = $allEmployees; + } + include 'employee_list.php'; + } catch (Exception $e) { + $errorMessage = "社員一覧の取得に失敗しました: " . $e->getMessage(); + include 'employee_list.php'; + } + } elseif ($action == 'register') { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + try { + $name = isset($_POST['name']) ? trim($_POST['name']) : ''; + $email = isset($_POST['email']) ? trim($_POST['email']) : ''; + + if ($name != '' && $email != '') { + $employeeAccess->add(array('name' => $name, 'email' => $email)); + header("Location: controller.php?action=list"); + exit; + } else { + $errorMessage = "名前とメールアドレスは必須です"; + } + } catch (InvalidArgumentException $e) { + $errorMessage = $e->getMessage(); + } catch (Exception $e) { + $errorMessage = "登録に失敗しました: " . $e->getMessage(); + } + } + include 'employee_register.php'; + } elseif ($action == 'update') { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + try { + $id = isset($_POST['id']) ? $_POST['id'] : ''; + $name = isset($_POST['name']) ? trim($_POST['name']) : ''; + $email = isset($_POST['email']) ? trim($_POST['email']) : ''; + + if ($id != '' && $name != '' && $email != '') { + $employeeAccess->update($id, array('name' => $name, 'email' => $email)); + header("Location: controller.php?action=list"); + exit; + } else { + $errorMessage = "ID、名前、メールアドレスは必須です"; + } + } catch (InvalidArgumentException $e) { + $errorMessage = $e->getMessage(); + } catch (RuntimeException $e) { + $errorMessage = $e->getMessage(); + } catch (Exception $e) { + $errorMessage = "更新に失敗しました: " . $e->getMessage(); + } + include 'employee_update.php'; + } else { + try { + $id = isset($_GET['id']) ? $_GET['id'] : ''; + if (empty($id)) { + $errorMessage = "IDが指定されていません"; + include 'employee_update.php'; + } else { + $employees = $employeeAccess->getAll(); + $employee = null; + foreach ($employees as $emp) { + if ($emp['id'] == $id) { + $employee = $emp; + break; + } + } + if ($employee === null) { + $errorMessage = "指定された社員が見つかりません"; + } + include 'employee_update.php'; + } + } catch (Exception $e) { + $errorMessage = "社員情報の取得に失敗しました: " . $e->getMessage(); + include 'employee_update.php'; + } + } + } elseif ($action == 'delete' && isset($_GET['id'])) { + try { + $id = $_GET['id']; + if (empty($id)) { + throw new InvalidArgumentException("IDが指定されていません"); + } + $employeeAccess->delete($id); + header("Location: controller.php?action=list"); + exit; + } catch (InvalidArgumentException $e) { + $errorMessage = $e->getMessage(); + include 'employee_list.php'; + } catch (RuntimeException $e) { + $errorMessage = $e->getMessage(); + header("Location: controller.php?action=list&error=" . urlencode($errorMessage)); + exit; + } catch (Exception $e) { + $errorMessage = "削除に失敗しました: " . $e->getMessage(); + header("Location: controller.php?action=list&error=" . urlencode($errorMessage)); + exit; + } + } else { + include 'index.php'; + } +} catch (Exception $e) { + // 初期化エラーなど、致命的なエラーの場合 + die("システムエラーが発生しました: " . htmlspecialchars($e->getMessage())); +} diff --git a/public/employee_list.php b/public/employee_list.php new file mode 100644 index 0000000..cf2638f --- /dev/null +++ b/public/employee_list.php @@ -0,0 +1,72 @@ + + + + + + 社員一覧 - 社員メンテナンスシステム + + + + + + + + +
+

社員一覧

+ + + + + + + + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + +
ID名前Email操作
+ 更新 + 削除 +
+ 新規登録 + ホームに戻る +
+ + + + + \ No newline at end of file diff --git a/public/employee_register.php b/public/employee_register.php new file mode 100644 index 0000000..6455d52 --- /dev/null +++ b/public/employee_register.php @@ -0,0 +1,44 @@ + + + + + + 社員登録 - 社員メンテナンスシステム + + + + + + + + +
+

社員登録

+ + + + +
+
+ + +
+
+ + +
+ + ホームに戻る +
+
+ + + + + \ No newline at end of file diff --git a/public/employee_update.php b/public/employee_update.php new file mode 100644 index 0000000..54de4bd --- /dev/null +++ b/public/employee_update.php @@ -0,0 +1,51 @@ + + + + + + 社員更新 - 社員メンテナンスシステム + + + + + + + + +
+

社員更新

+ + + + + +
+ +
+ + +
+
+ + +
+ + 社員一覧に戻る +
+ + + +
+ + + + + \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..a0a91ef --- /dev/null +++ b/public/index.php @@ -0,0 +1,30 @@ + + + + + + ホーム - 社員メンテナンスシステム + + + + + + + + +
+

ホーム

+ +
+ + + + + \ No newline at end of file diff --git a/ヒント.md b/ヒント.md new file mode 100644 index 0000000..6681f2e --- /dev/null +++ b/ヒント.md @@ -0,0 +1,219 @@ +# EmployeeAccess.php 実装のヒント + +## まず理解すべきこと:「配列の中に配列」 + +CSVファイルは「表形式」のデータです。これをPHPで扱うときは「配列の中に配列を入れる」形になります。 + +### イメージ + +``` +CSVファイル: +1610000001,山田太郎,yamada@gmail.com +1610000002,鈴木花子,suzuki@gmail.com +1610000003,麻枝史明,maeda@gmail.com + +↓ readData()で読み込むと... + +$rows = [ + ['1610000001', '山田太郎', 'yamada@gmail.com'], // 1行目 + ['1610000002', '鈴木花子', 'suzuki@gmail.com'], // 2行目 + ['1610000003', '麻枝史明', 'maeda@gmail.com'] // 3行目 +] +``` + +**重要**: `$rows`は「箱の中に箱が入っている」状態。外側の箱($rows)の中に、3つの小さな箱(各行)が入っている。 + +--- + +## add(新規登録)の考え方 + +### 質問:既にある配列に、新しい1行を追加するには? + +``` +今のCSVファイル($rows): +['1610000001', '山田太郎', 'yamada@gmail.com'] +['1610000002', '鈴木花子', 'suzuki@gmail.com'] +['1610000003', '麻枝史明', 'maeda@gmail.com'] + +↓ ここに新しい行を追加したい! + +['1610000004', '田中五郎', 'tanaka@example.com'] ← これを追加 +``` + +### 考え方の手順 + +1. **まず「箱の中身」を全部取り出す** + → `$rows = $this->readData();` + → 今、`$rows`には既存の3行が入っている + +2. **新しい1行(箱)を作る** + → 新しいID、名前、メールアドレスを1つの配列にする + → `$newRow = [新しいID, 新しい名前, 新しいメールアドレス];` + +3. **大きな箱($rows)の一番後ろに、小さな箱($newRow)を入れる** + → `$rows[] = $newRow;` + → これで$rowsには4行になる! + +4. **箱の中身をCSVファイルに全部書き戻す** + → `$this->writeData($rows);` + +### なぜ `$rows[] = $newRow;` と書くのか? + +**`配列[] = 値;` は「配列の最後に追加する」という意味** + +```php +$arr = [1, 2, 3]; +$arr[] = 4; +// 結果: $arr = [1, 2, 3, 4] + +$fruits = ['りんご', 'バナナ']; +$fruits[] = 'みかん'; +// 結果: $fruits = ['りんご', 'バナナ', 'みかん'] +``` + +CSVの場合も同じ: + +```php +$rows = [ + ['1', '山田', 'yamada@example.com'], + ['2', '鈴木', 'suzuki@example.com'] +]; + +$newRow = ['3', '田中', 'tanaka@example.com']; +$rows[] = $newRow; // ← 最後に追加! + +// 結果: +$rows = [ + ['1', '山田', 'yamada@example.com'], + ['2', '鈴木', 'suzuki@example.com'], + ['3', '田中', 'tanaka@example.com'] // ← 追加された +]; +``` + +**つまり**: 新しい行(箱)を、全体の配列(大きな箱)の最後にポンと入れるイメージ。 + +--- + +## update(更新)の考え方 + +### 質問:既にある行の、特定の項目だけを変更するには? + +``` +今のCSVファイル: +['1610000001', '山田太郎', 'yamada@gmail.com'] +['1610000002', '鈴木花子', 'suzuki@gmail.com'] ← この行の名前とメールを変更したい +['1610000003', '麻枝史明', 'maeda@gmail.com'] + +↓ + +['1610000001', '山田太郎', 'yamada@gmail.com'] +['1610000002', '鈴木次郎', 'suzuki_new@example.com'] ← 変更された! +['1610000003', '麻枝史明', 'maeda@gmail.com'] +``` + +### 考え方の手順 + +1. **箱の中身を全部取り出す** + → `$rows = $this->readData();` + +2. **1つずつ箱を開けて、目的のIDか確認する** + → forループで `$rows[0]`, `$rows[1]`, `$rows[2]` ... と順番に見ていく + → 各行の `$rows[$i][0]` がID + +3. **目的のIDが見つかったら、その行の名前とメールを上書きする** + → `$rows[$i][1] = 新しい名前;` + → `$rows[$i][2] = 新しいメールアドレス;` + +4. **見つかったらフラグを立てる** + → `$found = true;` + +5. **ループ後、見つからなければエラー** + → `if (!$found) throw new RuntimeException(...);` + +6. **箱の中身をCSVファイルに全部書き戻す** + → `$this->writeData($rows);` + +### 配列の要素を変更する方法 + +配列の中身を変更したい場合、いくつかの方法があります: + +```php +// 方法1: for文でインデックスを使う +for ($i = 0; $i < count($rows); $i++) { + if ($rows[$i][0] == '検索したいID') { + $rows[$i][1] = '新しい名前'; // インデックスで直接変更 + } +} + +// 方法2: foreachで参照渡し(&を使う) +foreach ($rows as &$row) { + if ($row[0] == '検索したいID') { + $row[1] = '新しい名前'; // &があるので元の配列が変わる + } +} +unset($row); // 重要: 参照を切断しないと、後で$rowを使ったときに元の配列が意図せず書き換わる + +// 方法3: foreachでキーを使う +foreach ($rows as $key => $row) { + if ($row[0] == '検索したいID') { + $rows[$key][1] = '新しい名前'; // キーで元の配列にアクセス + } +} +``` + +**注意**: 普通の `foreach ($rows as $row)` だと、`$row`はコピーなので元の`$rows`は変わりません。 + +--- + +## delete(削除)の考え方 + +### 質問:特定の行だけを除外した新しい配列を作るには? + +``` +今のCSVファイル: +['1610000001', '山田太郎', 'yamada@gmail.com'] +['1610000002', '鈴木花子', 'suzuki@gmail.com'] ← この行を削除したい +['1610000003', '麻枝史明', 'maeda@gmail.com'] + +↓ + +['1610000001', '山田太郎', 'yamada@gmail.com'] +['1610000003', '麻枝史明', 'maeda@gmail.com'] ← 2行だけ残った +``` + +### 考え方の手順 + +1. **箱の中身を全部取り出す** + → `$rows = $this->readData();` + +2. **新しい空の箱を用意する** + → `$newRows = [];` + +3. **1つずつ箱を開けて、削除対象じゃないものだけを新しい箱に入れる** + → foreachで各行をチェック + → IDが一致したら `continue;`(スキップ) + → 一致しなければ `$newRows[] = $row;`(新しい箱に入れる) + +4. **見つかったらフラグを立てる** + → `$found = true;` + +5. **ループ後、見つからなければエラー** + → `if (!$found) throw new RuntimeException(...);` + +6. **新しい箱の中身をCSVファイルに書き込む** + → `$this->writeData($newRows);` + +### なぜ `continue;` を使うのか? + +**「この行は無視して次へ」という意味** + +```php +foreach ($rows as $row) { + if ($row[0] == '削除したいID') { + continue; // ← ここで次のループへジャンプ。以下の処理はスキップされる + } + $newRows[] = $row; // ← 削除対象じゃない行だけここに到達する +} +``` + +**つまり**: 削除対象の行は「新しい箱に入れない」ことで削除を実現する。 diff --git a/仕様書/単体テスト仕様書.xlsx b/仕様書/単体テスト仕様書.xlsx new file mode 100644 index 0000000..bc51b79 Binary files /dev/null and b/仕様書/単体テスト仕様書.xlsx differ diff --git a/仕様書/結合テスト仕様書.xlsx b/仕様書/結合テスト仕様書.xlsx new file mode 100644 index 0000000..9cfd65a Binary files /dev/null and b/仕様書/結合テスト仕様書.xlsx differ