CakePHP3 で開発環境準備、改訂版


# インストール
まずは本体のインストール。

```bash
sudo composer create-project --prefer-dist cakephp/app myApp
```

無事終了したら、パーミッションを変えておく(OSXのみ)

```bash
sudo chown -R mymac:staff myApp
```

ここで一旦 .gitignore を編集して、 コミットしておく。
tmpとlogsをコメントアウトして、自分の環境でよくあるやつを入れておく。

```bash
cd myApp
vim .gitignore
```

```git:.gitignore
/vendor/*
/config/app.php
#/tmp/*
#/logs/*

.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
*.swp
*.un~
*~
*BAK
```

あとはいつものgit処理で。

```bash
git init
git add .
git commit -m "Initialize repository"
git remote add origin https://github.com/xxxxxxx/myApp.git
git push -u origin master
```

pushしたらtmpとlogsのコメントアウトを外しておく。

```git:.gitignore
/tmp/*
/logs/*
```

一旦サーバー起動してみて動作確認。

```bash
bin/cake server
```


http://localhost:8765/

# Composerの設定

composer.jsonを編集する。
suggest に入っている phpunit と codesniffer を require-dev に移す。
ついでに heroku も設定しておく。
あと、PDF生成もするかもしれないので tcpdf も一応いれておく。

```php:composer.json
    "require": {
		"tecnick.com/tcpdf": "*"
    },
    "require-dev": {
        "phpunit/phpunit": "*",
        "cakephp/cakephp-codesniffer": "*",
		"heroku/heroku-buildpack-php": "*"
    },
```


composerのアップデートをかける

```bash
sudo composer update
```

無事入ったら、phpunitの動作確認。

```bash
vendor/bin/phpunit
```

```bash
PHPUnit 4.8.6 by Sebastian Bergmann and contributors.



Time: 297 ms, Memory: 7.25Mb

No tests executed!
```

# データベース設定

この時点で（もっとそれより早くて構わないが）データベースを準備しておく。
テスト用のDBも作っておく。

```sql
CREATE DATABASE myapp_db DEFAULT CHARACTER SET utf8;
CREATE DATABASE test_myapp_db DEFAULT CHARACTER SET utf8;
```

config/app.php を編集してDB接続情報を入れておく。

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

        ],

        /**
         * The test connection is used during the test suite.
         */
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'root',
            'password' => 'password',
            'database' => 'test_myapp_db',
            'encoding' => 'utf8',
            //'timezone' => 'UTC',
            'cacheMetadata' => true,
            'quoteIdentifiers' => false,
        ],
    ],
```

# migration

migration を使って、一つのテーブルを軽く作る。（ここでは例としてArticlesテーブルを作っている）

```bash
bin/cake bake migration CreateArticles name:string created modified
```

config/Migrations 以下に migrationファイルが生成されるので、編集して、その他のカラム設定を追加し、テーブル定義を完成させる。

```bash
vim config/Migrations/20150826024512_create_articles.php
```

以下のURLを参考に。
http://book.cakephp.org/3.0/en/migrations.html

```php
<?php
use Phinx\Migration\AbstractMigration;

class CreateArticles extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $table = $this->table('articles');
        $table->addColumn('name', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('age', 'integer', [
            'default' => null,
            'limit' => 11,
            'null' => true,
        ]);
        $table->addColumn('posted_date', 'date', [
            'default' => null,
            'limit' => null,
            'null' => true,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->create();
    }
}
```


ファイルを保存したら、以下のコマンドでDBにテーブルを生成。


```bash
bin/cake migrations migrate
```

注）このやり方だと例えばmysql のview tableなんかを使っているとうまくいかない。
結局はMySQLに接続して直接操作した方がよいのかもしれない。
参考：http://qiita.com/yutaono/items/34bdc9383ce5870e7b8b#3-3
terminalから直接mysql接続できる。





続いて、以下のコマンドで、articlesテーブルのモデルをbakeコマンドで自動生成。
（するとtestsディレクトリ以下にテストケースとフィクスチャーも自動生成される）

```bash
bin/cake bake model Articles
```

再びテストしてみる。（以下でテスト結果が出るはず）


```bash
vendor/bin/phpunit
```

```bash
PHPUnit 4.8.6 by Sebastian Bergmann and contributors.

II

Time: 425 ms, Memory: 9.75Mb

OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 0, Incomplete: 2.
```

# コーディング規約

さらにコードスニッッファーの設定をする。
以下のコマンドを打つ。


```bash
sudo vendor/bin/phpcs --config-set installed_paths vendor/cakephp/cakephp-codesniffer
```

試してみる。

```bash
vendor/bin/phpcs --standard=CakePHP src/
```

エラーが出る(^^;


```bash
FILE: .../myApp/src/Model/Table/ArticlesTable.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 33 | ERROR | Expected 0 blank lines before closing function brace; 1
    |       | found
----------------------------------------------------------------------

Time: 398ms; Memory: 7.25Mb
```

直したきゃ直す。
vimの人は.vimrcを直しておくとよい。

```vim:.vimrc
set autoindent
set expandtab
set ts=4
set sw=4
```

# npm install

npmを使ってjs関係の設定をする。


```bash
npm init
```

いろいろ質問されるのでエンターキーを押す。
引き続き、以下をだーっとインストールする。


```bash
npm install jquery jquery-ui --save-dev
npm install bootstrap --save-dev
npm install grunt --save-dev
npm install grunt-phpunit --save-dev
npm install grunt-phpcs --save-dev
npm install time-grunt --save-dev
npm install grunt-browserify --save-dev
npm install grunt-contrib-watch --save-dev
npm install grunt-contrib-uglify --save-dev
npm install grunt-notify --save-dev
```

直下にpackage.jsonが作られるので編集して

```bash
vim package.json 
```


以下を追加。

```json:package.json
  "browser": {
    "jquery": "./node_modules/jquery/dist/jquery.js",
    "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.js",
    "jquery-ui": "./node_modules/jquery-ui/jquery-ui.js"
  }
```

# gruntの設定

自分で直下にGruntfile.jsを作成する。

```bash
vim Gruntfile.js
```


```js:Gruntfile.js
module.exports = function(grunt) {

	require('time-grunt')(grunt);

	grunt.initConfig({
		pkg: grunt.file.readJSON('package.json'),
		phpunit: {
			classes: {
				dir: 'tests/TestCase/'
			},
			options: {
				bin: 'vendor/bin/phpunit',
				bootstrap: 'tests/bootstrap.php',
				colors: true
			 }
		},
		phpcs: {
			application: {
				src: ['src/**/*.php']
			},
			options: {
				bin: 'vendor/bin/phpcs',
				standard: 'CakePHP'
			}
		},
		browserify : {
            dist : {
                src : 'src/Scripts/main.js',
                dest : 'webroot/js/build.js'
            }
        },
		uglify: {
            my_target: {
                options: {
                    sourceMap: true
                },
                files: {
                    'webroot/js/build.min.js': ['webroot/js/build.js']
                }
            }
        },
        watch: {
			phpcs: {
				files: ['src/**/*.php'],
				tasks: ['phpcs'],
				options: {
					spawn: false,
				},
            },
            browserify: {
                files: ['src/Scripts/**/*.js'],
                tasks: ['browserify', 'notify:browserify'],
                options: {
                    spawn: false,
                },
            },
			uglify: {
                files: ['src/Scripts/**/*.js'],
                tasks: ['uglify', 'notify:uglify'],
                options: {
                    spawn: false,
                },
            },
        },
        notify: {
            browserify: {
                options: {
                    message: 'Browserify finished running',
                }
            },
            uglify: {
               options: {
                   message: 'Uglify finished running'
               }
            }
        }
	});

	grunt.loadNpmTasks('grunt-notify');
	grunt.loadNpmTasks('grunt-phpunit');
	grunt.loadNpmTasks('grunt-phpcs');
	grunt.loadNpmTasks('grunt-browserify');
	grunt.loadNpmTasks('grunt-contrib-watch');
	grunt.loadNpmTasks('grunt-contrib-uglify');

	grunt.registerTask('default', ['watch']);
	grunt.registerTask('js', ['browserify', 'uglify']);
};
```

notify とかは完全に好みなので通知なんていらないって人は入れる必要ない。何をwatchするのかもその人の自由。


ちょっとここで動くかどうか確認。

```bash
grunt phpunit
```


# jsの雛形

src/Scripts というフォルダーを作って、その中にメインのjsを作成する。
jqueryとbootstrapとdatepickerを読み込む。


```bash
mkdir src/Scripts
vim src/Scripts/main.js
```

```js:src/Scripts/main.js
(function(){
	'use strict';
	
	window.jQuery = window.$ = require('jquery');
	require('bootstrap');
	require('jquery-ui/datepicker');
	
	var message = 'Hello App';
	console.log(message);
	
	$(".datepicker").datepicker({
        dateFormat: "yy-mm-dd"
    });

    $("#myModal").on('shown.bs.modal', function (e) {
        $(".datepicker").datepicker({
            dateFormat: "yy-mm-dd"
        });
    })

    $("#myModal").on('hidden.bs.modal', function (e) {
        $(this).removeData();
    });

})();
```

grunt が動くかどうか確認する。

```bash
grunt js
```

webroot/js/build.min.js が出来ているはずなので、レイアウトから読み込むようにしておく。
とりあえず /body の直上。

```html+php:src/Template/Layout/default.ctp
    <?= $this->Html->script('build.min') ?>
</body>
</html>
```

ついでにjquery-uiのcssを webroot/css 以下にコピーしておく。
cssは node_modules/jquery-ui/themes 以下にテーマ別においてあるので好きな物を選んでコピー。

レイアウトに以下追加。

```html+php:src/Template/Layout/default.ctp
    <?= $this->Html->css('jquery-ui.min.css') ?>
```

# コミット、他のメンバーの設定

.gitignoreに追加する。

```git:.gitignore
/node_modules/*
/webroot/files/*
```

add して commit して pushする。

```bash
git add .
git commit -m "Improve project file"
git push origin master
```

他の開発メンバーは、

```bash
git clone https://github.com/xxxxxxxxx/myApp.git
cd myApp/
sudo composer install
sudo vendor/bin/phpcs --config-set installed_paths vendor/cakephp/cakephp-codesniffer
npm install
```

データベースの設定も忘れずに。

```bash
vim config/app.php
```

# bakeで画面作り


見る画面がないのも寂しいのでbakeで作っておく。

```bash
bin/cake bake controller Articles
```

ビューも

```bash
bin/cake bake template Articles
```

一応確認。

```bash
bin/cake server
```

http://localhost:8765/articles


# heroku設定

で、herokuの準備。

直下にProcfileを作る。

```:Procfile
web: vendor/bin/heroku-php-apache2 webroot/
```


bootstrap.phpを編集。76行目あたりに以下を追加。

```php:config/bootstrap.php
if (isset($_ENV['CAKE_ENV'])) {
    Configure::load('app_' . $_ENV['CAKE_ENV'], 'default');
}
```

さらに、DebugKitのところを編集

```php:config/bootstrap.php
if (Configure::read('debug') && !isset($_ENV['CAKE_ENV'])) {
    Plugin::load('DebugKit', ['bootstrap' => true]);
}
```

app_heroku.phpを作成する。

```php:config/app_heroku.php
<?php
$db = parse_url(env('CLEARDB_DATABASE_URL'));
return [
    'debug' => false,
    'Security' => [
        'salt' => env('SALT'),
    ],
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => $db['host'],
            'username' => $db['user'],
            'password' => $db['pass'],
            'database' => substr($db['path'], 1),
            'encoding' => 'utf8',
            //'timezone' => 'UTC',
            'cacheMetadata' => true,
            'quoteIdentifiers' => false,
        ],
    ],
    'EmailTransport' => [
        'default' => [
            'className' => 'Smtp',
            // The following keys are used in SMTP transports
            'host' => 'ssl://' . $smtp['host'],
            'port' => $smtp['port'],
            'timeout' => 30,
            'username' => $smtp['user'],
            'password' => $smtp['pass'],
            'client' => null,
            'tls' => null,
        ],
    ],
    'Log' => [
        'debug' => [
            'className' => 'Cake\Log\Engine\ConsoleLog',
            'stream' => 'php://stdout',
            'levels' => ['notice', 'info', 'debug'],
        ],
        'error' => [
            'className' => 'Cake\Log\Engine\ConsoleLog',
            'stream' => 'php://stderr',
            'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
        ],
    ],
];
```

# heroku操作

で、heroku で作業。

```bash
heroku login
```

メールアドレスとパスワードを入れると、

```
Authentication successful.
```

になるはず。
引き続き、

```bash
heroku create
```

で、新しくアプリが作られる。

さらに、CakePHP用の環境変数設定。

```bash
heroku config:add CAKE_ENV="heroku"
```


SALT設定

```bash
heroku config:add SALT="123456789123456"
```

SMTPサーバーの設定

```bash
heroku config:add SMTP_SERVER="smtp://username:password@smtp.mail.hoge.jp:465"
```


ビルドパックをphpに設定。（これをやらないとnodejsになってしまう）


```bash
heroku buildpacks:set https://github.com/heroku/heroku-buildpack-php
```

herokuにプッシュ。

```bash
git push heroku master
```


データベースを使う。

```bash
heroku addons:create cleardb
```


シェルを起動して、

```bash
heroku run bash
```

マイグレ

```bash
bin/cake migrations migrate
```

マイグレを使わない場合は、

```bash
heroku config
```

と打つと、環境変数と共に、以下のような、

```bash
CLEARDB_DATABASE_URL:    mysql://b1234567890abc:12345678@aa-bbbb-cccc-dddd-01.cleardb.net/heroku_b123456789abcde?reconnect=true
```

接続情報が出るので、これを分解してmysqlで接続する。いや、別にそのまんまです。

```bash
mysql -u b1234567890abc -p -h aa-bbbb-cccc-dddd-01.cleardb.net heroku_b123456789abcde
```

接続できたら、普通に、create database して、create table して、とやる。
DBは以上。


ブラウザ起動！

```bash
heroku open
```

これで動くはず。

http://xxxxxxxxxx.herokuapp.com/articles/


# BASIC認証

とりあえずBASIC認証かけといてって必ず言われるので、以下をAppController.phpに仕込む。


```php:src/Controller/AppController.php
use Cake\Event\Event;

class AppController extends Controller
{
    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
        $this->autoRender = false;
        $loginId = 'admin';
        $password = 'pass';

        if (isset($_SERVER['PHP_AUTH_USER'])) {
            if (! ($_SERVER['PHP_AUTH_USER'] === $loginId && $_SERVER['PHP_AUTH_PW'] === $password)) {
                $this->_basicUnauthorized();
            }
        } else {
            $this->_basicUnauthorized();
        }
        $this->autoRender = true;
    }

    protected function _basicUnauthorized()
    {
        header('WWW-Authenticate: Basic realm="Please enter your ID and password"');
        header('HTTP/1.0 401 Unauthorized');
        die("Authorization Required");
    }
}
```


# bootstrap設定

とりあえずプラグインでテーマ設定する。

```bash
bin/cake bake plugin bootstrap
```

（ここでbootstrapというのは自分で決めたプラグインの名前である）
フォルダー plugins/Bootstrap/src　が作成されるので、そこにさらに自分でフォルダーを作る。

```bash
mkdir -p plugins/Bootstrap/src/Template/Bake/Template/
mkdir -p plugins/Bootstrap/src/Template/Bake/Element/
```

オリジナルのbake用のテンプレをコピーする。

```bash
cp vendor/cakephp/bake/src/Template/Bake/Template/* plugins/Bootstrap/src/Template/Bake/Template/
cp vendor/cakephp/bake/src/Template/Bake/Element/form.ctp plugins/Bootstrap/src/Template/Bake/Element/form.ctp
```

メニュー部分の高さ用のcssを作ってwebroot/css以下に保存する。
 
```css:webroot/css/menu.css
body {
  padding-top: 50px;
  padding-bottom: 20px;
}
```

bootstrap.min.css　もコピーしておく。

```bash
cp node_modules/bootstrap/dist/css/bootstrap.min.css webroot/css/bootstrap.min.css
```

レイアウトに追加

```html+php:default.ctp
    <?= $this->Html->css('bootstrap.min.css') ?>
    <?= $this->Html->css('menu.css') ?>
```

# formhelper の生成するタグを変える。

src/View/AppView.php の initialize のところに以下を追加する。

```php:src/View/AppView.php
    public function initialize()
    {
        $this->loadHelper('Form', [
            'templates' => 'app_form',
        ]);
    }
```

で、config 直下に自分で app_form.php というファイルを作る。

```php:/config/app_form.php
<?php
return [
    'inputContainer' => '<div class="form-control">{{content}}</div>',
    'label' => '<label{{attrs}} class="control-label">{{text}}</label>',
    'input' => '<input type="{{type}}" name="{{name}}"{{attrs}} class="form-control">'
];
```


自分がいじりたい部分だけ、配列に書いて、修正を入れる。
オリジナルは

```
vendor/cakephp/cakephp/src/View/Helper/FormHelper.php
```

にだーっと書いてある。


あとはbake する時に、

```bash
bin/cake bake template Users -t Bootstrap
```

とする。


# 最新版にする

```bash
sudo composer update
npm update
```


# テストを書く

とりあえずコントローラの方のbakeで自動生成されているテストに何も考えずに書く。

```php:tests/TestCase/Controller/ArticlesControllerTest.php
use Cake\ORM\TableRegistry;
```

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testIndex()
    {
        $this->get('/articles?page=1');

        $this->assertResponseOk();
    }
```

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testView()
    {
        $this->get('/articles/view/1');

        $this->assertResponseOk();
    }
```

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testAdd()
    {
        $data = [
            'name' => '記事',
            'age' => 19,
            'posted_date' => '2015-08-12'
        ];
        $this->post('/articles/add', $data);

        $this->assertResponseSuccess();

        $articles = TableRegistry::get('Articles');
        $query = $articles->find()->where(['name' => $data['name']]);
        $this->assertEquals(1, $query->count());
    }
```

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testEdit()
    {
        $this->get('/articles/edit/1');

        $this->assertResponseOk();
    }
```

削除はステータス302で移動するのでassertResponseSuccessにしている。

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testDelete()
    {
        $this->post('/articles/delete/1');

        $this->assertResponseSuccess();
    }
```

ちなみにBASIC認証をかけてしまった場合は、以下を追加

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function setUp()
    {
        parent::setUp();

        $_SERVER['PHP_AUTH_USER'] = 'admin';
        $_SERVER['PHP_AUTH_PW'] = 'pass';
    }
```

# file upload

jquery-file-upload を使うことにする。composer にあるので、composer.json に追記。

```json
{
     "require": {
         "blueimp/jquery-file-upload": "*"
    },
     "autoload": {
        "psr-4": {
             "UploadHandler\\": "./vendor/blueimp/jquery-file-upload/server/php"
        },
        "classmap": ["./vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php"]
    },
 }
```

composer をアップデートかける。

```bash
sudo composer update
```


新規にUploadsController.php を作る。

```php:src/Controller/UploadsController.php
<?php
namespace App\Controller;

use App\Controller\AppController;
use UploadHandler;

/**
 * Uploads Controller
 *
 */
class UploadsController extends AppController
{
    /**
     * initialize method
     *
     * @return void
     */
    public function initialize()
    {
        parent::initialize();
        $this->loadComponent('RequestHandler');
    }

    /**
     * index method
     *
     * @return void
     */
    public function index()
    {
        $this->autoRender = false;
        $uploadHandler = new UploadHandler();
    }
}
```

あとはビューにファイル参照ボタンを追加して、

```html+php:src/Template/Articles/add.ctp
    <style type="text/css">
    .bar {
        height: 18px;
        background: green;
    }
    </style>

    <?= $this->Form->create($article, ['type' => 'file']) ?>
    <input id="fileupload" type="file" name="files[]" data-url="/uploads/index" multiple>
    <div id="progress">
        <div class="bar" style="width: 0%;"></div>
    </div>
    <div id="files"></div>
```


JavaScriptも適当に追加

```js:main.js
    $('#fileupload').fileupload({
        dataType: 'json',
        done: function (e, data) {
            $.each(data.result.files, function (index, file) {
                $('<p/>').text(file.name).appendTo('#files');
            });
        },
        progressall: function (e, data) {
            var progress = parseInt(data.loaded / data.total * 100, 10);
            $('#progress .bar').css(
                'width',
                progress + '%'
            );
        }
    }).prop('disabled', !$.support.fileInput)
        .parent().addClass($.support.fileInput ? undefined : 'disabled');

```

webrootの下にアップロードするファイル用のフォルダーを作っておく。

```bash
mkdir webroot/files
```


とりあえず動いたってところまで。

## JSON出力

routes.php に以下を追記。

```php:config/routes.php
Router::extensions(['json', 'xml']);
```

これで、単に今までのコントローラのURLに `.json` を付けるだけで結果がJSONになる。
やってみてびびったのだが、http://book.cakephp.org/3.0/en/views/json-and-xml-views.html#creating-json-views にあるように、

```php
    public function initialize()
    {
        parent::initialize();
        $this->loadComponent('RequestHandler');
    }
```

を追加する必要はない。
bakeで作った素のコントローラ、

```php:src/Controller/ArticlesController.php
    public function view($id = null)
    {
        $article = $this->Articles->get($id, [
            'contain' => []
        ]);
        $this->set('article', $article);
        $this->set('_serialize', ['article']);
    }
```

これそのまんまでいいのだ。呼ぶ時に `.json` をつければJSONになり、つけなければビューが呼ばれる。
まあ、なんかちょっと気味が悪いが、これでよしとしてテストを追加する。

```php:tests/TestCase/Controller/ArticlesControllerTest.php
    public function testJson()
    {
        $this->configRequest([
            'headers' => ['Accept' => 'application/json']
        ]);
        $result = $this->get('/articles/view/1.json');

        $this->assertResponseOk();

        $expected = [
            'article' =>
            ['id' => 1, 'name' => '山田太郎', 'age' => '18', 'posted_date' => '2015-10-15', ・・・],
        ];
        $expected = json_encode($expected, JSON_PRETTY_PRINT);
        $this->assertEquals($expected, $this->_response->body());
    }
```

expectedにはfixtureの値を入れておく。