CakePHP3をWindowsにインストールしてチュートリアル前編まで進める

下記のように公式の解説が充実していますが、途中で結構とまどってしまいました。
ここではフレームワークの詳しい解説は抜きにして、チュートリアルで作るブックマークアプリの作業手順とスクリーンショットを淡々と記録しています。
私と同じ環境ならば、本文に従って進めることでブックマークアプリができあがるはずです。

- 参照: [Cookbook | クイックスタートガイド](https://book.cakephp.org/3.0/ja/quickstart.html)

- - -
## 環境
- Windows 10
- CakePHP 3.4.1
- XAMPP 7.1.1 / PHP 7.1.1 ([導入済み](https://www.apachefriends.org/download.html))
- Composer 1.3.2 ([Windows用インストーラで導入済み](https://github.com/composer/windows-setup/releases/))
- Webサーバー: XAMPPのApacheを使用
- DB: XAMPPのMySQLを使用

- - -
## インストール
### 注意: intl拡張を有効にする
公式解説にもあるように、XAMPPではphp.iniの下記の行のコメントアウトを解除しなければなりません。

- 参照: [Cookbook | インストール | システム要件](https://book.cakephp.org/3.0/ja/installation.html#id2)

```diff:php.ini(899行目)
- ;extension=php_intl.dll
+ extension=php_intl.dll
```

これを忘れると、下記のようにインストールが失敗していしまいます。
「lib-icuが必要だ」と警告されます。

```:コマンドプロンプト
>composer self-update && composer create-project --prefer-dist cakephp/app my_cake3_test
You are already using composer version 1.3.2 (stable channel).
Installing cakephp/app (3.4.0)
  - Installing cakephp/app (3.4.0) Downloading: 100%
Created project in my_cake3_test
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - cakephp/cakephp 3.4.1 requires lib-icu >=4.8 -> the requested linked library icu has the wrong version installed or is missing from your system, make sure to have the extension providing it.
    - cakephp/cakephp 3.4.0 requires lib-icu >=4.8 -> the requested linked library icu has the wrong version installed or is missing from your system, make sure to have the extension providing it.
    - Installation request for cakephp/cakephp 3.4.* -> satisfiable by cakephp/cakephp[3.4.0, 3.4.1].
```

### Composerでインストール
intl拡張を有効にすれば、インストールは下記のように滞りなく進みます。
最後に、フォルダのパーミッション設定について尋ねられます。
`y`を押して許可します。

```:コマンドプロンプト
>composer self-update && composer create-project --prefer-dist cakephp/app cake3_test

(省略)

Set Folder Permissions ? (Default to Y) [Y,n]?
```

下の画像のようなCakePHPのホーム画面が表示されれば、インストールは成功です。
(ちなみに、私の場合はApacheのドキュメントルートの下の"cake3/"にインストールしたので、URLは`http://localhost/cake3/cake3_test`となります)

![01-CakePHPホーム画面.png](https://qiita-image-store.s3.amazonaws.com/0/27816/887040bb-2e39-f8f8-f0c9-4b808b935a1c.png)

なお、データベースに接続できないと警告されますが、これは次章で設定するのでそのままで構いません。

![02-データベースは無効.png](https://qiita-image-store.s3.amazonaws.com/0/27816/b530b13a-e68d-a386-0bda-75f1bb9813bc.png)

- - -
## データベースの準備
### データベースとテーブルを作る
phpMyAdminで新しいデータベースを作り、下記で紹介されているSQLを貼り付けて実行します。

- 参照: [Cookbook | クイックスタートガイド | データベースの作成](https://book.cakephp.org/3.0/ja/quickstart.html#id4)

```sql:SQL
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created DATETIME,
    modified DATETIME
);

CREATE TABLE bookmarks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(50),
    description TEXT,
    url TEXT,
    created DATETIME,
    modified DATETIME,
    FOREIGN KEY user_key (user_id) REFERENCES users(id)
);

CREATE TABLE tags (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    created DATETIME,
    modified DATETIME,
    UNIQUE KEY (title)
);

CREATE TABLE bookmarks_tags (
    bookmark_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (bookmark_id, tag_id),
    FOREIGN KEY tag_key(tag_id) REFERENCES tags(id),
    FOREIGN KEY bookmark_key(bookmark_id) REFERENCES bookmarks(id)
);
```

今回はDB名は「cake3_test」としました。
下記のようにテーブルが4つ作成されれば成功です。

![04-テーブル作成.png](https://qiita-image-store.s3.amazonaws.com/0/27816/9b389254-e252-db16-2278-f3d9facec98d.png)

### データベースの接続設定をする
公式解説に従ってapp.phpを修正します。
phpMyAdminがデフォルトのままなら、`username`と`password`は下記のようにします。

```diff:config\app.php
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
+           'username' => 'root',
+           'password' => '',
            'database' => 'cake3_test',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
        ],
```

ホーム画面を更新して、下記のようにDB接続が有効になっていれば成功です。

![03-データベースが有効に.png](https://qiita-image-store.s3.amazonaws.com/0/27816/793ccede-dbbd-1291-cadf-f2690df6d369.png)

- - -
## Scaffoldコードを生成する
### bakeのインストール
まず、下記に従ってアプリフォルダ内にbakeをインストールします。

- 参照: [Cookbook | Bakeコンソール](https://book.cakephp.org/3.0/ja/bake.html)

```:コマンドプロンプト
>composer require --dev cakephp/bake:~1.0
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 0 installs, 1 update, 0 removals
  - Updating cakephp/bake (1.2.14 => 1.3.0) Downloading: 100%
Writing lock file
Generating autoload files
> Cake\Composer\Installer\PluginInstaller::postAutoloadDump
```

### コードを生成する
下記に従い、スラッシュをバックスラッシュに替えた上でコマンドを実行します。

- 参照: [Cookbook | クイックスタートガイド | Scaffold コードの生成](https://book.cakephp.org/3.0/ja/quickstart.html#scaffold)

```:コマンドプロンプト
bin\cake bake all users
bin\cake bake all bookmarks
bin\cake bake all tags
```

下記のように末尾で「Bake All complete.」と表示されれば成功です。

```:コマンドプロンプト
>bin\cake bake all users

Welcome to CakePHP v3.4.1 Console

(省略)

Bake All complete.
```

### 各ページの表示を確認する
ホーム画面のURLに`/users`, `/bookmarks`, `/tags`をそれぞれ追加して、下記のような画面が表示されれば成功です。
(私の場合は`http://localhost/cake3/cake3_test/users`)

![05-users画面.png](https://qiita-image-store.s3.amazonaws.com/0/27816/5120a66b-702d-adab-276e-4d1b5dc70481.png)

- - -
## パスワードのハッシュ化
### ハッシュ化しないとどうなるか
まず、ハッシュ化しないとどうなるかを確認します。
`http://localhost/cake3/cake3_test/users`でユーザーを作成する際に、パスワードを`abcd1234`とします。
すると、アプリでもphpMyAdminでも、パスワードが平文のままであることが分かります。

![06-平文_画面.png](https://qiita-image-store.s3.amazonaws.com/0/27816/c67219de-0cda-5da9-7df1-8dbc5f4f2701.png)

![07-平文_MySQL.png](https://qiita-image-store.s3.amazonaws.com/0/27816/4f13fccb-9694-d85f-49fa-7bd67c3066c7.png)

### ハッシュ化する
下記に従って、`src\Model\Entity\User.php`にコードを追加します。

- 参照: [Cookbook | クイックスタートガイド | パスワードハッシュを追加](https://book.cakephp.org/3.0/ja/quickstart.html#id6)

```diff:src\Model\Entity\User.php
    <?php
    namespace App\Model\Entity;

+   use Cake\Auth\DefaultPasswordHasher;
    use Cake\ORM\Entity;

    class User extends Entity
    {
    (省略)

+   /**
+    * パスワードのハッシュ化
+    */
+   protected function _setPassword($value)
+   {
+       $hasher = new DefaultPasswordHasher();
+       return $hasher->hash($value);
+   }
```

なお、`_setPassword`というメソッド名は下記に基づくようです。

> アクセサーは _get + フィールド名のキャメルケースという命名ルールを使います。

- 参照: [Cookbook | エンティティ | アクセサーとミューテーター](https://book.cakephp.org/3.0/ja/orm/entities.html#id5)

### ハッシュ化を確認する
もう一度パスワードを`abcd1234`で保存すると、今度は暗号化されているはずです。

![08-ハッシュ化_画面.png](https://qiita-image-store.s3.amazonaws.com/0/27816/7b1014c7-e1f6-4868-6a16-328913ffc67d.png)

![09-ハッシュ化_MySQL.png](https://qiita-image-store.s3.amazonaws.com/0/27816/ede3b81d-a46a-c91c-3d2c-f6a30b8f6335.png)

- - -
## タグによる検索機能の追加
### ルーティングを追加
下記に従って、`config/routes.php`に追記します。

- 参照: [Cookbook | クイックスタートガイド | タグを指定してブックマークを取得](https://book.cakephp.org/3.0/ja/quickstart.html#id7)

```diff:config\routes.php
    Router::defaultRouteClass(DashedRoute::class);

+   // 新しいルートを　tagged アクションのために追加します。
+   // 末尾の `*` は、渡された引数を持っていることを
+   // CakePHP に伝えます。
+   Router::scope(
+       '/bookmarks',
+       ['controller' => 'Bookmarks'],
+       function ($routes) {
+           $routes->connect('/tagged/*', ['action' => 'tags']);
+       }
+   );

    Router::scope('/', function (RouteBuilder $routes) {
```

- 関連情報: [Cookbook | ルーティング](https://book.cakephp.org/3.0/ja/development/routing.html)

この状態で`http://localhost/cake3/cake3_test/bookmarks/tagged`にアクセスすると、下記のような警告が表示されます。
tagsアクションが存在しないこと、そしてそれをどこで定義すればいいかを丁寧に教えてくれています。
これには感心しました。

![10-ルーティングを追加.png](https://qiita-image-store.s3.amazonaws.com/0/27816/da8925b0-94c2-8ac9-1353-57e34ed57e4f.png)

### tagsアクションを追加
公式解説と先程の警告に従って、`src\Controller\BookmarksController.php`にコードを追加します。

```diff:src\Controller\BookmarksController.php
    class BookmarksController extends AppController
    {

+       /**
+        * タグ検索用のメソッド
+        */
+       public function tags()
+       {
+           // CakePHP によって提供された 'pass' キーは全ての
+           // リクエストにある渡された URL のパスセグメントです。
+           $tags = $this->request->param('pass');

+           // タグ付きのブックマークを探すために BookmarksTable を使用
+           $bookmarks = $this->Bookmarks->find('tagged', [
+               'tags' => $tags
+           ]);

+           // ビューテンプレートに変数を渡します
+           $this->set([
+               'bookmarks' => $bookmarks,
+               'tags' => $tags
+           ]);
+       }
```

ここで画面を更新すると、今度は「tagged」という名のFinderメソッドが存在しないことを教えてくれます。

![11-tagsアクション追加.png](https://qiita-image-store.s3.amazonaws.com/0/27816/14646fd8-996b-50e7-72dd-3c650c471eec.png)

### Finderメソッドの作成
公式解説と先程の警告に従って、`src\Model\Table\BookmarksTable.php`にコードを追加します。

- 参照: [Cookbook | クイックスタートガイド | Finder メソッドの作成](https://book.cakephp.org/3.0/ja/quickstart.html#finder)

```diff:src\Model\Table\BookmarksTable.php
    class BookmarksTable extends Table
    {
+       // $query 引数は、クエリービルダーのインスタンスです。
+       // $options 配列には、コントローラのアクション中で find('tagged') に渡した
+       // 'tag' オプションが含まれます。
+       public function findTagged(Query $query, array $options)
+       {
+           $bookmarks = $this->find()
+               ->select(['id', 'url', 'title', 'description']);

+           if (empty($options['tags'])) {
+               $bookmarks->leftJoinWith('Tags', function ($q) {
+                   return $q->where(['Tags.title IS ' => null]);
+               });
+           } else {
+               $bookmarks->innerJoinWith('Tags', function ($q) use ($options) {
+                   return $q->where(['Tags.title IN' => $options['tags']]);
+               });
+           }

+           return $bookmarks->group(['Bookmarks.id']);
+       }
```

ここで画面を更新すると、今度はビューファイルがないと警告されます。

![12-Finderメソッド追加.png](https://qiita-image-store.s3.amazonaws.com/0/27816/7a0a736a-ffcb-4291-7d5f-3477ef92680d.png)

### ビューの作成
公式解説と先程の警告に従って、`src\Template\Bookmarks`内に新たに`tags.ctp`を作成します。

- 参照: [Bookmark | クイックスタートガイド | ビューの作成](https://book.cakephp.org/3.0/ja/quickstart.html#id9)

```:src\Template\Bookmarks\tags.ctp
<h1>
    Bookmarks tagged with
    <?= $this->Text->toList(h($tags)) ?>
</h1>

<section>
<?php foreach ($bookmarks as $bookmark): ?>
    <article>
        <!-- Use the HtmlHelper to create a link -->
        <h4><?= $this->Html->link($bookmark->title, $bookmark->url) ?></h4>
        <small><?= h($bookmark->url) ?></small>

        <!-- Use the TextHelper to format text -->
        <?= $this->Text->autoParagraph(h($bookmark->description)) ?>
    </article>
<?php endforeach; ?>
</section>
```

ここで画面を更新すると、ようやく警告のない状態になります。

![13-ビュー作成.png](https://qiita-image-store.s3.amazonaws.com/0/27816/b4063929-81df-9f1a-ef9d-16ace1d9d8fa.png)

### タグ検索の動作確認
テストのため、Bookmarksで「Qiita」と「GitHub」を、Tagsで「プログラマ向け」を作成して、タグをブックマークに関連させます。
その上で`http://localhost/cake3/cake3_test/bookmarks/tagged/プログラマ向け`にアクセスすると、下記のように表示されるはずです。
これで、チュートリアル前編まで完了しました。

![14-タグ検索.png](https://qiita-image-store.s3.amazonaws.com/0/27816/7beeeca2-5190-7a2b-10e9-6ac2c09d94f7.png)