CakePHP3でWebアプリ開発

# 概要
本記事では、CakePHPを使って、顧客管理システムを作ることを目的として、システムへの認証機能から順を追って説明していきます。

# 前提
CakePHPがインストールされていること。

# 下準備
アプリを新規作成する。

```sh
$ composer create-project --prefer-dist cakephp/app [アプリ名]
```

TIPSとして、上記コマンドはやや長いので、エイリアスを貼ることをおすすめします。

```sh
$ alias cakenewapp='composer create-project --prefer-dist cakephp/app'
$ cakenewapp [アプリ名]
```

MySQLに新規データベースを作成する。

```mysql
mysql> create database [データベース名]
Query OK, 1 row affected (0.03 sec)
```

config設定を変更する。(config/app.php)

```php
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            /**
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'nonstandard_port_number',
            'username' => 'my_app', <=これ
            'password' => 'secret', <=これ
            'database' => 'my_app', <=これ
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'log' => false,
```

組み込みのWEBサーバを起動して、テストページにアクセスし、設定内容が問題無いか確認する。

```sh
$ bin/cake server -H 0.0.0.0
```

# 認証機能の実装
MVCを順に作成していくことで、認証機能が簡単に実装できます。（作成する順番としては、M->C->Vですが）

## Model
userテーブルを作成する。

```mysql
mysql> CREATE TABLE users (
    ->     id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ->     username VARCHAR(50),
    ->     password VARCHAR(255),
    ->     role VARCHAR(20),
    ->     created DATETIME DEFAULT NULL,
    ->     modified DATETIME DEFAULT NULL
    -> );
Query OK, 0 rows affected (0.38 sec)
```

UsersTableクラスを定義するため、```src/Model/Table/UsersTable.php```を作成する。

```php

<?php

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{

    public function validationDefault(Validator $validator)
    {
        return $validator
            ->notEmpty('username', 'A username is required')
            ->notEmpty('password', 'A password is required')
            ->notEmpty('role', 'A role is required')
            ->add('role', 'inList', [
                'rule' => ['inList', ['admin', 'user']],
                'message' => 'Please enter a valid role'
            ]);
    }

}
```

Userクラスのエンティティを定義するため、```src/Model/Entity/User.php```を作成する。(Usersではなく、Userであることに注意！)

```php
<?php

namespace App\Model\Entity;

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

class User extends Entity
{

    protected $_accessible = [
        '*' => true,
        'id' => false
    ];

    protected function _setPassword($password)
    {
        return (new DefaultPasswordHasher)->hash($password);
    }
}
```

## Controller
認証機能を有効にするため、AppControler(```src/Controller/AppController.php```)を修正する。

```php
    public function initialize() /* modify */
    {
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
                'authorize' => ['Controller'],
                'loginRedirect' => [
                    'controller' => 'Users',
                    'action' => 'index'
                ],
                'logoutRedirect' => [
                    'controller' => 'Users',
                    'action' => 'login'
                ]
        ]);
    }

    public function isAuthorized($user) /* add */
    {
        return false;
    }
```

UsersControllerを定義するため、```src/Controller/UsersController.php```を作成する。

```php
<?php

namespace App\Controller;

use App\Controller\AppController;
use Cake\Event\Event;

class UsersController extends AppController
{

    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
        $this->Auth->allow(['add', 'logout']);
    }

    public function login()
    {
        if ($this->request->is('post')) {
            $user = $this->Auth->identify();
            if ($user) {
                $this->Auth->setUser($user);
                return $this->redirect($this->Auth->redirectUrl());
            }
            $this->Flash->error(__('Invalid username or password, try again'));
        }
    }

    public function logout()
    {
        return $this->redirect($this->Auth->logout());
    }

    public function isAuthorized($user) 
    {
        return true;
    }   
    
    public function index()
    {
        $this->set('users', $this->Users->find('all'));
    }

    public function view($id)
    {
        $user = $this->Users->get($id);
        $this->set(compact('user'));
    }

    public function add()
    {
        $user = $this->Users->newEntity();
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->data);
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));
                return $this->redirect(['action' => 'add']);
            }
            $this->Flash->error(__('Unable to add the user.'));
        }
        $this->set('user', $user);
    }

}
```

## View
ユーザ登録画面のテンプレートを定義するため、```src/Template/Users/add.ctp```を作成する。

```html+php
<div class="users form">
<?= $this->Form->create($user) ?>
    <fieldset>
        <legend><?= __('Add User') ?></legend>
        <?= $this->Form->input('username') ?>
        <?= $this->Form->input('password') ?>
        <?= $this->Form->input('role', [
            'options' => ['admin' => 'Admin', 'user' => 'user']
        ]) ?>
   </fieldset>
<?= $this->Form->button(__('Submit')); ?>
<?= $this->Form->end() ?>
</div>
```

ログイン画面のテンプレートを定義するため、```src/Template/Users/login.ctp```を作成する。

```html+php
<div class="users form">
<?= $this->Flash->render('auth') ?>
<?= $this->Form->create() ?>
    <fieldset>
        <legend><?= __('Please enter your username and password') ?></legend>
        <?= $this->Form->input('username') ?>
        <?= $this->Form->input('password') ?>
    </fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
```

ユーザ一覧画面のテンプレートを定義するため、```src/Template/Users/index.ctp```を作成する。

```html+php
<h1>users</h1>
<table>
    <tr>
        <th>Id</th>
        <th>Username</th>
    </tr>
    <?php foreach ($users as $user): ?>
    <tr>
        <td><?= $user->id ?></td>
        <td><?= $this->Html->link($user->username, ['action' => 'view', $user->id]) ?></td>
    </tr>
    <?php endforeach; ?>
</table>
```

ユーザ詳細画面のテンプレートを定義するため、```src/Template/Users/view.ctp```を作成する。

```html+php
<h1><?= h($user->id) ?></h1>
<p><?= h($user->username) ?></p>
```

これで、簡単に認証の仕組みができあがります！

[参考：Cookbook ブログチュートリアル](http://book.cakephp.org/3.0/en/tutorials-and-examples/blog-auth-example/auth.html)

# 顧客管理機能の実装

認証機能が実装できたので、顧客管理機能を追加します。

## Model
customersテーブルを作成する。

```sql
CREATE TABLE customers (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(20) NOT NULL,
    last_name VARCHAR(20) NOT NULL,
    telephone_number VARCHAR(20) NOT NULL,
    mailaddress VARCHAR(50) NOT NULL,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);
```

CustomersTableクラスを定義するため、```src/Model/Table/CustomersTable.php```を作成する。

```php
<?php

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\Validation\Validator;

class CustomersTable extends Table
{

    public function validationDefault(Validator $validator)
    {
        return $validator
            ->notEmpty('first_name', 'First name is required')
            ->notEmpty('last_name', 'Last name is required');
    }

}
```

Customerクラスのエンティティを定義するため、```src/Model/Entity/Customer.php```を作成する。

```php
<?php

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Customer extends Entity
{

    protected $_accessible = [
        '*' => true,
        'id' => false
    ];

}
```

## Controller
CustomersControllerを定義するため、```src/Controller/CustomersController.php```を作成する。

```php
<?php

namespace App\Controller;

use App\Controller\AppController;
use Cake\Event\Event;

class CustomersController extends AppController
{

    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
    }

    public function isAuthorized($user) 
    {
        return true;
    }   
    
    public function index()
    {
        $this->set('customers', $this->Customers->find('all'));
    }

    public function view($id)
    {
        $customer = $this->Customers->get($id);
        $this->set(compact('customer'));
    }

    public function add()
    {
        $customer = $this->Customers->newEntity();
        if ($this->request->is('post')) {
            $customer = $this->Customers->patchEntity($customer, $this->request->data);
            if ($this->Customers->save($customer)) {
                $this->Flash->success(__('The customer has been saved.'));
                return $this->redirect(['action' => 'add']);
            }
            $this->Flash->error(__('Unable to add the customer.'));
        }
        $this->set('customer', $customer);
    }

}
```

## View
顧客登録画面のテンプレートを定義するため、```src/Template/Customers/add.ctp```を作成する。

```html+php
<div class="customers form">
<?= $this->Form->create($customer) ?>
    <fieldset>
        <legend><?= __('Add Customer') ?></legend>
        <?= $this->Form->input('first_name') ?>
        <?= $this->Form->input('last_name') ?>
        <?= $this->Form->input('telephone_number') ?>
        <?= $this->Form->input('mailaddress') ?>
   </fieldset>
<?= $this->Form->button(__('Submit')); ?>
<?= $this->Form->end() ?>
</div>
```

顧客一覧画面のテンプレートを定義するため、```src/Template/Customers/index.ctp```を作成する。

```html+php
<h1>Customers</h1>
<table>
    <tr>
        <th>Id</th>
        <th>First name</th>
        <th>Last name</th>
    </tr>
    <?php foreach ($customers as $customer): ?>
    <tr>
        <td><?= $this->Html->link($customer->id, ['action' => 'view', $customer->id]) ?></td>
        <td><?= $customer->first_name ?></td>
        <td><?= $customer->last_name ?></td>
    </tr>
    <?php endforeach; ?>
</table>
```

顧客詳細画面のテンプレートを定義するため、```src/Template/Customers/view.ctp```を作成する。

```html+php
<h1><?= h($customer->id) ?></h1>
<p><?= h($customer->first_name) ?></p>
<p><?= h($customer->last_name) ?></p>
<p><?= h($customer->telephone_number) ?></p>
<p><?= h($customer->mailaddress) ?></p>
```

# 注文管理機能の実装

顧客管理機能が実装できたので、さらに顧客からの注文を管理する機能を追加します。

## Model

requestsテーブルを作成する。
1人の顧客が複数の注文をできるという要件で実装してみるため、requestsテーブルにcustomer_idを持たせて、外部キーを設定します。

```sql
CREATE TABLE requests (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    customer_id INT UNSIGNED NOT NULL,
    created DATETIME,
    modified DATETIME,
    FOREIGN KEY customer_key (customer_id) REFERENCES customers(id)
);
```

RequestsTableクラスを定義するため、```src/Model/Table/RequestsTable.php```を作成する。

```php
<?php

namespace App\Model\Table;

use Cake\ORM\Table;

class RequestsTable extends Table
{
    public function initialize(array $config)
    {
        $this->addBehavior('Timestamp');
    }
}
```

## Controller

RequestsControllerを定義するため、```src/Controller/RequestsController.php```を作成する。

```php
<?php

namespace App\Controller;

class RequestsController extends AppController
{

    public function initialize()
    {
        parent::initialize();

        $this->loadComponent('Flash'); // Include the FlashComponent
    }

    public function isAuthorized($user)
    {
        return true;
    }

    public function index()
    {
        $requests = $this->Requests->find('all');
        $this->set(compact('requests'));
    }

    public function view($id = null)
    {
        $request = $this->Requests->get($id);
        $this->set(compact('request'));
    }

    public function add()
    {
        $request = $this->Requests->newEntity();
        if ($this->request->is('post')) {
            $request = $this->Requests->patchEntity($request, $this->request->query);
            if ($this->Requests->save($request)) {
                $this->Flash->success(__('Your request has been saved.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to add your request.'));
        }
        $this->set('request', $request);
    }
}

```

顧客詳細画面に注文のリストを表示するため、CustomersControllerのviewメソッドを修正する。

```php
use Cake\ORM\TableRegistry;
・
・
・
    public function view($id = null)
    {
        $customer = $this->Customers->get($id);
        $this->set(compact('customer'));

        $requests = TableRegistry::get('Requests');
        $this->set('requests', $requests->find('all'));
    }
```

## View

注文登録画面のテンプレートを定義するため、```src/Template/Requests/add.ctp```を作成する。

```html+php
<h1>Add Request</h1>
<?php
    echo $this->Form->create($request);
    echo $this->Form->button(__('Save Request'));
    echo $this->Form->end();
?>
```

注文一覧画面のテンプレートを定義するため、```src/Template/Requests/index.ctp```を作成する。

```html+php
<h1>requests</h1>
<table>
    <tr>
        <th>Id</th>
        <th>CustomerId</th>
    </tr>

    <?php foreach ($requests as $request): ?>
    <tr>
        <td>
            <?= $this->Html->link($request->id, ['action' => 'view', $request->id]) ?>
        </td>
        <td>
            <?= $this->Html->link($request->customer_id, ['controller' => 'customers', 'action' => 'view', $request->customer_id]) ?>
        </td>
    </tr>
    <?php endforeach; ?>
</table>
```

注文詳細画面のテンプレートを定義するため、```src/Template/Requests/view.ctp```を作成する。

```html+php
<h1><?= h($request->id) ?></h1>
<p>CustomerId: <?= h($request->customer_id ?></p>
```

さらに顧客詳細画面のテンプレートに、その顧客に属する注文リストを追加する。

```html+php
<h1><?= h($customer->title) ?></h1>
<p>FirstName: <?= h($customer->first_name) ?></p>
<p>LastName: <?= h($customer->last_name) ?></p>
<p>TelephoneNumber: <?= h($customer->telephone_number) ?></p>
<p>Mailaddress: <?= h($customer->mailaddress) ?></p>

<p><?= $this->Html->link('Add Request', ['controller' => 'requests', 'action' => 'add', '?' => ['customer_id' => $customer->id]]) ?></p>

<p>requests</p>
<table>
    <tr>
        <th>Id</th>
    </tr>

    <?php foreach ($requests as $request): ?>
    <tr>
        <td> <?= $this->Html->link($request->id, ['action' => 'view', $request->id]) ?> </td>
    </tr>
    <?php endforeach; ?>
</table>
```

こちらの記事を参考にすすめていくと、簡単に管理機能の実装ができあがります。
[Blog Tutorial part2](http://book.cakephp.org/3.0/en/tutorials-and-examples/blog/part-two.html)
[Blog Tutorial part3](http://book.cakephp.org/3.0/en/tutorials-and-examples/blog/part-three.html)

今後も引き続き追加していきます。。。