# ch02 — ファイルを読む

Source: https://php-school.pages.dev/topics/file-io/ch02-read-file/
Site: AIと学ぶPHPの学校 (https://php-school.pages.dev)
Lesson: ファイル入出力
Level: L4

Reference (official): https://www.php.net/manual/ja/book.filesystem.php

## 本文

# ch02 — ファイルを読む

## 概要

PHP には「ファイルを読む」関数が **手軽さ重視** と **制御性重視** の 2 系統ある。手軽さの代表が **`file_get_contents($path)`**。パスを渡すだけで **ファイル全体を 1 つの文字列として返してくれる**。設定ファイルや小さなテキストならこの 1 行で十分。

もう一方の制御性重視が **`fopen` + `fgets` + `fclose`**。`fopen('path', 'r')` でハンドルを取り、`fgets($fp)` で **1 行ずつ** 読み、用が済んだら `fclose($fp)` で閉じる。手間は増えるが、ループしながら少しずつ処理できるので **ギガ単位の大きなファイル** でもメモリを食い潰さない。「メモリと手軽さのトレードオフ」と覚えると選び分けが楽。

ファイル系関数では **パスの組み立て** にも注意。スクリプトの隣にあるファイルを指したいとき、ただ `'data.txt'` と書くとカレントディレクトリ依存で事故る。`__DIR__ . '/data.txt'` のように **`__DIR__` を起点に組む** のがイディオム (`__DIR__` は「このファイルが置かれているディレクトリの絶対パス」)。ch05 でパス操作を本格的に扱うが、ここでも先取りで使う。

## 学習目標

- `file_get_contents()` で **ファイル全体** を 1 つの文字列として読める
- `fopen` + `fgets` + `fclose` の組み合わせで **1 行ずつ** 読める
- `__DIR__ . '/tests/data/foo.txt'` のように **スクリプトからの相対パス** を組める
- 読み込んだテキストの **末尾改行** に気をつけて出力できる

## 所要時間

スライド = 5 分 / ドリル = 約 20 分

## ドリル

| # | 内容 |
|---|---|
| `01-read-all/` | `tests/data/hello.txt` の中身をそのまま出力 |
| `02-read-line-by-line/` | `fgets` で 1 行ずつ読み、先頭に **行番号** を付けて出力 |

## 採点

```bash
php scripts/grade.php topics/file-io/ch02-read-file/drill/01-read-all/
php scripts/grade.php topics/file-io/ch02-read-file/drill/02-read-line-by-line/
```

## 確認のコツ

- 読み込み元は **drill 内固定パス** (`__DIR__ . '/tests/data/...'`)。リポジトリにコミットされているデータを読み取る形
- このチャプターでは **書き込みは一切しない** (ch03 から)

## スライド

---
marp: true
theme: dojo
paginate: true
size: 16:9
---

<!--
LLM_CONTEXT:
  Lesson 14 / Chapter 2
  目的: テキストファイルを読む 2 系統 (file_get_contents / fopen+fgets+fclose) を手に馴染ませる
  扱わない: 書き込み (ch03) / CSV (ch04) / パス分解 (ch05) / アップロード (ch06)
  読み上げ時間目安: 5 分
-->

# ファイルを読む

Lesson 14 / Chapter 2

---

## 2 つの読み方を持っておく

| 関数 | どんな時 |
|---|---|
| `file_get_contents($path)` | **小さい / 中身を文字列でまとめて欲しい** とき |
| `fopen` + `fgets` + `fclose` | **大きい / 1 行ずつ処理したい** とき |

```php
<?php
// パターン A: 一発で全部
$text = file_get_contents(__DIR__ . '/tests/data/hello.txt');

// パターン B: 1 行ずつ
$fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
while (($line = fgets($fp)) !== false) {
    echo $line;   // $line には末尾の \n も含まれる
}
fclose($fp);
```

**「設定読み込みは A」「ログ走査は B」** くらいの肌感で使い分けると良い。

---

## `file_get_contents` の挙動

```php
$path = __DIR__ . '/tests/data/hello.txt';
$text = file_get_contents($path);

var_dump($text);
// string(36) "こんにちは
// ファイル読み込みのテスト
// "
```

- 成功すると **ファイル全体を文字列で返す**
- 失敗すると **`false`** を返す (パス間違い・権限なしなど)
- そのまま `echo` すれば中身がそのまま出る (末尾改行も含む)

→ **「末尾に `\n` が含まれてる前提」** で出力を組み立てる。`echo $text . "\n"` のように追加で改行を足すと **空行が増える** 罠あり。

---

## `fopen` / `fgets` / `fclose` の作法

```php
<?php
$fp = fopen(__DIR__ . '/tests/data/hello.txt', 'r');
if ($fp === false) {
    fwrite(STDERR, "open failed\n");
    exit(1);
}

while (($line = fgets($fp)) !== false) {
    // $line には末尾の改行 (\n / \r\n) が含まれる
    echo rtrim($line) . "\n";
}

fclose($fp);
```

| ステップ | 役割 |
|---|---|
| `fopen($path, 'r')` | ファイルを **読み込みモード** で開く |
| `fgets($fp)` | **1 行読む** (末尾改行付き)。EOF で `false` |
| `fclose($fp)` | ファイルを **閉じる** (OS のリソースを返す) |

`fclose` を忘れても PHP 終了時に解放されるが、**「開いたら閉じる」** を癖にする。

---

## なぜ `fgets` のループは `while (($line = fgets($fp)) !== false)` なのか

```php
// ❌ よくある罠: feof で書くと最後の行を 1 個多く読む
while (!feof($fp)) {
    $line = fgets($fp);   // EOF 直前で fgets が false を返すが、ループは 1 回余分に回る
    echo $line;
}

// ✅ 安全: fgets の戻り値を直接判定
while (($line = fgets($fp)) !== false) {
    echo $line;
}
```

- `feof` は **直前の読み取りで EOF を踏んだ後** に true になる → 「false が来てから初めて気づく」
- 戻り値で判定すれば **EOF を踏んだ瞬間に抜ける**

→ 「**fgets の戻り値で判定**」を defalut 形として覚える。

---

## パスは `__DIR__` から組む

```php
// ❌ NG: cwd (現在ディレクトリ) に依存する
$text = file_get_contents('tests/data/hello.txt');

// ✅ OK: スクリプト自身のディレクトリ基準
$text = file_get_contents(__DIR__ . '/tests/data/hello.txt');
```

- 採点ランナー (`php scripts/grade.php drill/01-read-all/`) は **リポジトリルートから起動** される
- cwd 相対パスだと、起動位置がズレた瞬間 `false` (= パス間違い) で落ちる
- **`__DIR__` を基準** にすれば、どこから呼ばれてもファイルを見つけられる

→ file-io トピックでは **「パスは必ず `__DIR__` から組む」** が鉄則。

---

## このチャプターでできるようになること

✅ `file_get_contents` で 1 発でテキストを読める
✅ `fopen` / `fgets` / `fclose` で 1 行ずつ読める
✅ `fgets` ループは **戻り値判定 (`!== false`)** で書ける
✅ パスは **`__DIR__` 基準** で組める
✅ 読み込んだ文字列の **末尾改行** を意識して echo できる

→ ドリルへ
