【独習PHP第4版】本書の環境XAMPPをDockerで実現

『独習PHP第4版』の環境構築はXAMPPですが、既にDockerをインストールしていたため。これで一冊最後まで学習ができたので記録。
# 目次
[1.先にまとめ](#1-先にまとめ)
[2.起動したら必ずやること](#2-起動したら必ずやること)
[3.dockerのため本書と違うところ](#3-dockerのため本書と違うところ)
# 1. 先にまとめ
【使用環境】
・OS : Windows10
・Docker Desktop : 4.16.2
・Docker Engine: 20.10.22
・WSL2(Ubuntu) : Ubuntu 20.04.5 LTS (Focal Fossa)
・VSCode : 1.75.0
## 全体図
```
selfphp
  |---mariadb
  |    |___data                         //空ディレクトリ
  |    |___Dockerfile
  |---php
  |    |___docker-php-ext-xdebug.ini
  |    |___Dockerfile
  |    |___myphp.ini
  |---web                               //ドキュメントルート(後述)。PHPのコードはこの下に。
  |    |___parts                        //呼び出して使う関数はここに保管
  |    |     |___DbManager.php          //PHPとデータベースを接続するためのファイル
  |    |___index.php                    //好きに作業する
  |___docker-compose.yml
```
## MariaDB（MySQL）
```docker:Dockerfile
FROM mariadb:10.7.7-focal
EXPOSE 3306
```
MySQLの場合は`FROM mysql:8.0-debian`とした。
## PHP
```ini:docker-php-ext-xdebug.ini
[xdebug]
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
;デバッグを実行すると、PHPコンテナ内の/tmp/xdebug.logにログが出力される。
xdebug.log=/tmp/xdebug.log
xdebug.client_port=9003
```
```docker:Dockerfile
FROM php:8.2-rc-apache
# ① 「user」という名の一般ユーザーを作成。
ARG USERNAME=user
ARG GROUPNAME=user
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID $GROUPNAME && \
    useradd -m -s /bin/bash -u $UID -g $GID $USERNAME
# ②「libzip-dev」「unzip」「pdo_mysql」「zip」「xdebug」「mhsendmail」をインストール＆有効化
RUN apt-get update \
  && apt-get install -y --no-install-recommends libzip-dev unzip \
  && docker-php-ext-install pdo_mysql zip \
  && pecl install xdebug \
  && docker-php-ext-enable xdebug \
  && curl -sSL https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 -o mhsendmail \
  && chmod +x mhsendmail \
  && mv mhsendmail /usr/local/bin/mhsendmail
# ③ ComposerをPHPコンテナ内に取り入れる
COPY --from=composer/composer:latest-bin /composer /usr/bin/composer
# ④ 自作の設定ファイル「myphp.ini」「docker-php-ext-xdebug.ini」をPHPコンテナに適用させる
COPY myphp.ini /usr/local/etc/php/conf.d
COPY docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
# ⑤ PHPコンテナ内に入った時のカレントディレクトリを指定
WORKDIR /var/www/html
EXPOSE 80
```
【参考】
①https://qiita.com/Spritaro/items/602118d946a4383bd2bb
③[Composer公式ドキュメント](https://getcomposer.org/doc/00-intro.md)「Docker Image」の項にて。イメージはdockerが提供している「composer」でなく、Composer公式の「composer/composer」を使用。『dockerのイメージは、公式が直接公開したものではないので、最新版を受け取るのに数日遅れる。バイナリのみの同等のものがない』らしい。
```ini:myphp.ini
[Core]
sendmail_path = "/usr/local/bin/mhsendmail --smtp-addr=mailhog:1025"
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.language = "Japanese"
[Session]
;session使用時、実データがPHPコンテナ内の/tmp下に保存される。
session.save_path = "/tmp"
;以下は、本書で紹介されている「推奨値」を設定。
session.cookie_httponly = "On"
session.cookie_samesite = "Lax"
session.sid_length = "48"
session.sid_bites_per_character = "6"
session.use_strict_mode = "On"
```

## web
```php:DbManager.php
<?php
$dsn = 'mysql:dbname=selfphp; host=db; charset=utf8';
$usr = 'root';
$passwd = 'myrootpassword';
try {
  $db = new PDO($dsn, $usr, $passwd);
  print '接続に成功しました。';
}catch (PDOException $e) {
  die("接続エラー:{$e->getMessage()}");
} finally {
  $db = null;
}
```
このコードは『独習PHP第4版』から拝借。
## docker-compose.yml
```yml:docker-compose.yml
version: '3'
services:
  app:
    build:
      context: ./php
      dockerfile: Dockerfile
    volumes:
      - type: bind
        source: "./web"
        target: "/var/www/html"
    ports:
      - "80:80"
    depends_on:
      - db
  db:
    build:
      context: ./mariadb
      dockerfile: Dockerfile
    volumes:
      - type: bind
        source: "./mariadb/data"
        target: "/var/lib/mysql"
    ports:
      - "3306:3306"
    environment:
      # MySQLのイメージを使用するなら、環境変数は「MYSQL_ROOT_PASSWORD」に。
      - MARIADB_ROOT_PASSWORD=myrootpassword
  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
```
* 通常の起動・停止
`docker-compose up -d`で起動、 `docker-compose down`で停止。この起動はキャッシュを使うので、もしファイルを修正したら、起動時は`docker-compose up -d --build`でビルドから始める。
* コンテナ名
`docker-compose`コマンドで起動したら、コンテナ名は「作業中のディレクトリ名-コンテナ名-1」となる。例えばPHPコンテナ名は「selfphp-app-1」。（`docker ps`で確認可）
* ウェブ上で表示できるか確認する
・ `index.php`にアクセスするには「`http://localhost:80/index.php`」。
・ `web`と`/var/www/html`のマウントによって、ウェブで公開する部分（ドキュメントルート）を設定している。（[PHPイメージのドキュメント](https://hub.docker.com/_/php)「php:\<version>-apache」の項にて）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/16f4b12f-ac26-a421-c91b-b1bc34c5c73d.png)
・ ブラウザ上でのパスを確認したいなら、`print __FILE__;`で出力するとよい。
* `mariadb/data`と`/var/lib/mysql`のマウント
データを保持するため。[MariaDBイメージのドキュメント](https://hub.docker.com/_/mariadb)より「Where to Store Data」の項。コンテナを停止する度にコンテナは削除される。データベースやテーブルなどのデータは、MariaDBコンテナ内で作成するのでデータも都度消えてしまう。マウントすることでデータを常に保持できる。
* コンテナ内に入る。`docker exec -it コンテナ名 /bin/bash`
* PHPコンテナ内での作業
・ Composerを使ってパッケージをインストールする時。
・ sessionが正しく機能しているかチェックする時（コンテナ内の`/tmp`を`ls`で確認）。
・ デバッグのログを見たい時（コンテナ内の`/tmp/xdebug.log`に出力される）。
* MariaDBコンテナ内での作業
・ データベースやテーブルを作成したり、SQLを実行する時。
・ ~~PHPとデータベースを接続するためにIPアドレスを確認する時。~~
# 2. 起動したら必ずやること
:::note
①launch.jsonに"pathMappings"を**追記する。**
（②`zend_extension`の値を書き直す。）
~~③PHPとデータベースを接続する時、DockerではhostのIPアドレスを**毎回確認する。**~~
④Composerを利用する時は、PHPコンテナに**ユーザーを指定して**入る。
:::
簡単です！
## ①launch.jsonに"pathMappings"を追記する。
これはデバッグを実行するために必要な作業。
事前に、VSCodeの拡張機能をインストールする所から「PHP Debug」を検索し、インストールしておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/75558db0-58e3-1ca4-a84b-ca1ff5737e01.png)
（1）初めての方は、虫マーク→「create a launch.json file.」→「PHP」を選択。`selfphp`下に`.vscode/launch.json`ファイルが自動作成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/05963b2a-8231-3b4f-9f0d-b1dec06010a3.png)
（2）`launch.json`ファイルを見て、portが「9003」であることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/03bd5f52-f2eb-1817-8286-8da5b2a03812.png)
そしてこの`"port: 9003"`の下に以下のコードを追記。
```json:launch.json
      "pathMappings": {
        "/var/www/html":"${workspaceFolder}/web"
      }
```
* 左側（`/var/www/html`）はPHPコンテナのドキュメントルートのパス。
* 右側（`${workspaceFolder}/web`）は作業中のドキュメントルートのパス。
`docker-compose.yml`ファイルからも分かるように、`/var/www/html`とマウントしている部分。`${workspaceFolder}`は今、VSCodeで開いているディレクトリ（selfphpディレクトリ）。
▼【完成した`launch.json`】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/caab2ace-e392-df10-40c9-02b03bd98e1f.png)
（3）正しくデバッグできるか確認する。
簡単にチェック用のindex.phpを作成する。このコードは『独習PHP第4版』から拝借。
```php:index.php
<?php
$msg = 'こんにちは、世界！';
print $msg;
```
デバッグしたいファイル`index.php`を開いた状態で虫マーク→「Listen for Xdebug」であることを確認して横の三角をクリック（デバッグ開始）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/b0178c5b-6b84-253d-6c88-1754dbe9ce2d.png)
↓このマークが出る。ちなみに終了は一番右の四角をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/cc360a71-a06e-afa5-bbda-4bc6f2154c8c.png)
`index.php`のコード左側をクリックしてブレークポイントを設置。ブレークポイントの行は**実行せず**、一時停止する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/20c05330-cc7d-af62-37b9-70a9375e6030.png)
ブラウザから`http://localhost:80/index.php`で`index.php`にアクセス→VSCodeに戻る。下のようにブレークポイントで止まっていればOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/921b932f-f29b-57ed-c06e-c1deaf7da393.png)
ブレークポイントの行は実行されずに止まるので、`$msg`の値はまだ初期化されていないと出る。これでデバッグは問題なく使用できる。

【参考】
・https://zenn.dev/ikeo/articles/244d6a8042bcd8c55fe9
・https://qiita.com/gigosa/items/90431be7a6a79db78480
## （②zend_extensionのパスを書き換える）
* [Xdebug公式のドキュメント](https://xdebug.org/docs/install)「Configure PHP」の項にて、もしXdenugが表示されなかったり、ファイルが見つからないと出たらXdebugのファイルパスを明示的に設定する。
* PHPコンテナ内に入り、`cd`で`/usr/local/lib/php/extensions`あたりまで行く。`ls`と`cd`で追ってパスを確認する。
```ini:docker-php-ext-xdenug.ini
[xdebug]
zend_extension=xdebug
```
↓
```ini:docker-php-ext-xdenug.ini
[xdebug]
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20210903/xdebug.so
```
ファイルを変更したので、Dockerを停止→`docker-compose up -d --build`で起動する。

## ~~③PHPとデータベースを接続する時、DockerではhostのIPアドレスを毎回確認する。~~
こんなことしなくてもよいみたいです。
hostの部分は、MariaDbコンテナの「IPアドレス」ではなく「コンテナ名」を書けばOK。コンテナ名は、`docker-compose.yml`ファイルの`services`にある。ここでMariaDbコンテナを`db`と名付けた。正式名`selfphp-db-1`を使ってもどちらでもよい。
【修正前】host=172.19.0.2
【修正後】host=db
```php:DbManager.php
<?php
$dsn = 'mysql:dbname=selfphp; host=db; charset=utf8';
$usr = 'root';
$passwd = 'myrootpassword';
try {
  $db = new PDO($dsn, $usr, $passwd);
  print '接続に成功しました。';
}catch (PDOException $e) {
  die("接続エラー:{$e->getMessage()}");
} finally {
  $db = null;
}
```
~~hostにはMariaDbコンテナのIPアドレスを書く。起動すると変わることがあるので**毎回確認する。**
（1）起動後、docker exec -it selfphp-db-1 /bin/bashでMariaDBコンテナに入る。
（2）cat /etc/hostsで確認。~~
```
127.0.0.1       localhost
         ~省略~
172.23.0.2      12ab34cd56ef    ←こっち。つまり172.23.0.2をhostに書く。
```
~~一番下、MariaDBのコンテナIDと共に表示されている方のIPアドレス。同じものをhostに書く。~~
* `mysql:dbname=selfphp`の「selfphp」とは、MariaDBで作成したデータベース名。Dockerの場合、MariaDBコンテナに入る→`mariadb -p`（MySQLの場合は`mysql -p`）→パスワード入力（`docker-compose.yml`の`MARIADB_ROOT_PASSWORD`で設定済）でMariaDBを起動。`CREATE DATABASE selfphp;`でデータベースを作成。テーブルも作っておく。
* `$passwd`は、先ほど入力したパスワード。


（3）`http://localhost:80/parts/DbManager.php`でファイルにアクセスし、「接続に成功しました。」が出ればOK。
## ④Composerを利用する時は、PHPコンテナにユーザーを指定して入る。
Composerを利用する時は、`docker exec -it -u user selfphp-app-1 /bin/bash`でPHPコンテナに入ること。
* コンテナに入る時は、`-u(--user) user`**でユーザーを指定しないとrootユーザーで入ることになる。** そのため、コンテナ内で作業して作られたファイルなどが全てroot権限で、後で全く編集できなくなり困った。
* そもそもComposerのドキュメントに[rootで実行するな](https://getcomposer.org/doc/faqs/how-to-install-untrusted-packages-safely.md)と書かれている。なので、PHPのDockerfileで「user」という名のユーザーを作成した。これがないとrootでしか入れない。
* ちなみにroot権限で作業してしまい、ファイルが編集できなくて詰んだ時は…userに権限を変更する。以下のコマンドをそのまま実行。
（1）`chown -R user:user ./`
カレントディレクトリ下の使用者権限を全て変更。`user:user`の部分は、ユーザー名：グループ名。
（2）`find . -type f -print | xargs chmod 644`
カレントディレクトリ下の、ファイルの実行権限を全て644に変更。
（3）`find . -type d -print | xargs chmod 755`
カレントディレクトリ下の、ディレクトリの実行権限を全て755に変更。

【参考】https://cpoint-lab.co.jp/article/202206/22876/
# 3. dockerのため本書と違うところ
## ①mb_send_mail()を使ってメール送信
※ 本書では、XAMPPに元から入っている「sendmail」を使って実際にメールを送信しますが、僕の知識不足のため、導入が簡単な「[MailHog](https://hub.docker.com/r/mailhog/mailhog)」と「[mhsendmail](https://github.com/mailhog/mhsendmail)」を使いました。学習に支障はありませんが、本書通りにできたら追記します（検証中）。
※ もし「sendmail」を使いたい方は、PHPコンテナにインストールする必要があるので、PHPのDockerfileに「sendmail」を追記してください。
```dockerfile:Dockerfile
~ 省略 ~
apt-get install -y --no-install-recommends libzip-dev unzip sendmail \
```
### MailHogの使い方は簡単
* 「MailHog」は、PHPのアプリでメール機能をつける時、mb_send_mail関数を使う時などで、**実際にメールが届いた側としてメールの状況を確認できる**（本文の表示崩れなど）。
* 実際の宛先には送信せず、「MailHog」に送信する。「MailHog」にメールを送信するために「mhsendmail」が必要なので、両方インストール。「sendmail」でも可能らしいが未検証。


（1）`http://localhost:8025`で「MailHog」を開く。ここで受信したメール、メールヘッダーが確認できる。
（2）mb_send_mail関数で、メールを送信するファイルを作成（本書に手順があるので省略）

:::note warn
本書の書き方と変えた所
* mb_send_mail関数のメールヘッダー（`$headers`）は配列で渡した。PHP7.2以降から可能。これはメールヘッダーインジェクションの防止になる。
* メールヘッダー（`$headers`）の配列に`'Content-Type'=>'text/plain; charset=UTF-8', 'Content-Transfer-Encoding'=>'8bit'`を追記。mb_send_mail関数で送信した文字コードは日本語（ISO-2022-JP）だったが、「MailHog」では文字化けするので、UTF-8を指定した。
:::
【参考】https://teratail.com/questions/318732

（3）そのファイルにアクセスする。または、PHPコンテナ内に入って`php ファイル名`で実行する。→（1）「MailHog」を見る。
* Fromヘッダーの値の書き方
 ①差出人の名前で表示したい・・・差出人名\<from\@example.com>
②メールアドレスで表示する・・・from@example\.com
こんな感じで届きます↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/d1cab290-a900-d48b-aad4-9fc04ff06d7c.png)

## ②PHPDocの使い方
* [PHPDocumentor](https://docs.phpdoc.org/3.0/guide/getting-started/index.html)（以下PHPDoc）は、コードから自動で必要な情報を取り出し、仕様書として整形してくれる。
* 整形したいファイルにあらかじめドキュメンテーションコメントを書く必要があるが、書き方は本書や[ドキュメント](https://docs.phpdoc.org/3.0/guide/guides/docblocks.html)などにあるので省略。

インストールの方法は[3つ](https://docs.phpdoc.org/3.0/guide/getting-started/installing.html#installation)。
1. dockerイメージを使う。→僕はDockerなのでこの方法
1. Phiveを使い、依存関係としてインストールする。
1. PHARファイル（.phar）をダウンロードし、好みの場所に配置する。→本書はこの方法

※Composerを使ってもインストールできるが[非推奨](https://phpdoc.org/)。PHPDoc側とこちら側の依存関係でコンフリクトが起こる可能性がある。
### PHPDocのdockerイメージを使う
（1）このツールを使用すると、いくつかディレクトリが作成されるので、1つにまとまっていた方が綺麗だなー…と思い、`web`ディレクトリに`phpdoc`ディレクトリを作った（ (3)の図）。ここをカレントディレクトリとして作業する。
（2）カレントディレクトリで以下のコマンドを実行。エイリアス（別名）を作成。
`alias phpdoc="docker run --rm -v $(pwd):/data phpdoc/phpdoc:3"`
* 自作したコマンド「`phpdoc`」だけでPHPDocコンテナの起動/マウント/削除を行う。「=」の前後にスペースを入れないこと！
* このエイリアスはその場限りで、エディタを閉じると設定が消えるので注意。`alias`と打つと、設定したエイリアス一覧を確認できる。すると、`$(pwd)`の部分が絶対パスに置き換わっている。

（3）カレントディレクトリに、「doc」「out」というディレクトリを作成する。
「doc」には、ドキュメンテーションコメントを書いたファイルを置こう。
```
selfphp
   :
   |___web
   |    |___phpdoc（カレントディレクトリ）
   |          |___doc             //ドキュメント化したいファイルはここに置く。
   |          |    |___ファイル1
   |          |    |___ファイル2
   |          |___out             //空ディレクトリ。作成されたドキュメントが自動でここに入る
   :
```
（4）カレントディレクトリで`phpdoc -d doc -t -out`を実行する。
* `-d`は、ドキュメント化したいファイルが入っているディレクトリ。
* `-f`でファイルを指定することもできる。
* `-t`は、生成されたドキュメントを入れる場所（ディレクトリ）。Target Folderと呼ぶ。

【参考】
・https://docs.phpdoc.org/3.0/guide/guides/running-phpdocumentor.html

（5）「out」下に作成された、`index.html`にアクセスする。すると自分で作ったファイルがドキュメント化されている。

以上となります。最後まで読んでいただき、ありがとうございました。