[CakePHP]CakePHP3.8のチュートリアル

 当ページのリンクには広告が含まれています。

やっぱり基本ってすごく大事で。
遠回りしてるようで結局1番の近道だなとしみじみ感じてます。

DBと各種設定

設定開始。

        'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
        'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', 'UTC'),
        'defaultLocale' => env('APP_DEFAULT_LOCALE', 'ja_JP'),
        'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', '+09:00'),

次。
ソルト書き換え。適当でいいけど長いほどいい。

    'Security' => [
        'salt' => env('SECURITY_SALT', '0EBBdcPB3kXvEjoFtCvsc6BPKmb8W4hHuBsV5XAJ'),
    ],
    'Datasources' => [
        'default' => [
            'className' => Connection::class,
            'driver' => Mysql::class,
            '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' => 'non_standard_port_number',
            'username' => 'my_app',
            'password' => 'secret',
            'database' => 'my_app',
            /*
             * You do not need to set this flag to use full utf-8 encoding (internal default since CakePHP 3.6).
             */
            //'encoding' => 'utf8mb4',
            'timezone' => 'UTC',
            'flags' => [],
            'cacheMetadata' => true,
            'log' => false,

            /**
             * Set identifier quoting to true if you are using reserved words or
             * special characters in your table or column names. Enabling this
             * setting will result in queries built using the Query Builder having
             * identifiers quoted when creating SQL. It should be noted that this
             * decreases performance because each query needs to be traversed and
             * manipulated before being executed.
             */
            'quoteIdentifiers' => false,

            /**
             * During development, if using MySQL < 5.6, uncommenting the
             * following line could boost the speed at which schema metadata is
             * fetched from the database. It can also be set directly with the
             * mysql configuration directive 'innodb_stats_on_metadata = 0'
             * which is the recommended value in production environments
             */
            //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],

            'url' => env('DATABASE_URL', null),
        ],

host, username, password, databaseを書き換え。
timezone+09:00に書き換え。

date_default_timezone_set(Configure::read('App.defaultTimezone'));
date_default_timezone_set('Asia/Tokyo');

ページを表示させると、

無事にDBへ接続完了。

各ファイルの作成

何故か公式サイトがまともに表示されないので、チュートリアル記事を検索

bakeしないの?

先にテーブル作ったし、bakeしてテンプレファイル一括生成しない?って発想に至るかもしれません。個人的には、慣れるまでbakeしない方がいいです。
というのも、結局泥臭い方法が1番習得には早道で。

コード手打ち > コピペ > bake

この順番でやるのが言語の習得が早いし、どの方法を取るにしても毎回ソースを読んで理解できているか、確認して行かないと学習が進んで躓くんですよね。

あと今の段階でbakeできるほどきちんとテーブル設計できる人は、公式サイトで十分だったり見なくても今までの経験で何となく書けると思うw

基本ファイル作成

Controller, Model, Viwe(Template)ファイルを作成します。
CakePHPは規約をとても重視しているので、決まったルールに基づいてファイル名を決めると自動的に各ファイルが紐づきます。

DBでarticlesテーブルを作成したので、src/Model/Table/の中にArticlesTable.phpを作成します。

Model

<?php
namespace App\Model\Table;

use Cake\ORM\Table;

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

Modelは、DBのデータをやり取りしたり加工したりします。
$this->addBehavior('Timestamp')は、articlesテーブルのcreated, modifiedに自動で日時を入れるコードです。
※DBのカラム側に設定も必要ですが、コピペしてれば上手く行くので今回は割愛

Controller

<?php
namespace App\Controller;

class ArticlesController extends AppController
{

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

ここではまずindexというactionを作成しています。
Controllerで設定したactionはデフォルトではwww.example.com/articles/indexというURLでアクセス出来ます。
同様に、 foobar() という関数を定義すると、ユーザーは、www.example.com/articles/foobar でアクセス出来ます。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

www.example.com/articles/about のURLでページを表示させるならpublic function about()があるし、www.example.com/articles/stocks ならpublic function stocks()があります。

$this->set(compact('articles');は上の行で設定している$articlesを配列にしてviewへ渡す機能です。
compact()はもともとphpにある関数です。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

compact()は簡単に言うと、変数を一気に配列にする関数です。
特にフレームワークは最後に変数を全部viewに渡さないといけないので、compact()は頻出です。

参考:PHP: compact – Manual

Viwe(Template)

<html>
<head></head>
<body>
<h1>ブログ記事</h1>
<table>
    <tr>
        <th>Id</th>
        <th>タイトル</th>
        <th>作成日時</th>
    </tr>
    <?php foreach ($articles as $article) : ?>
        <tr>
            <td><?= $article->id ?></td>
            <td>
                <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>
            </td>
            <td>
                <?= $article->created->format('y/m/d'); ?>
            </td>
        </tr>
    <?php endforeach; ?>
</table>
</body>
</html>

Viewファイルはコントローラーで作成したactionの内容をURLで表示させたい時にそのアクション名を使用して作成します。
今回のindexであれば作成場所はsrc/Template/Articles/index.ctpとなります。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

参考サイトにもある通り、ArticlesController.phphogeというアクションが作成されていた場合、そのactionに紐づけるviewファイルはsrc/Template/Articles/hoge.ctpになるし、ArticlesController.phptestというアクションが作成されていた場合、そのactionに紐づけるviewファイルはsrc/Template/Articles/test.ctpになります。

<?php foreach ($articles as $article) : ?>

上記でコントローラーから$this->set(compact('articles');で受け取った値をforeach ($articles as $article)で回して全件表示しています。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita
 <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>

上記はヘルパーというcakeの機能を使用してidごとの詳細ページへのリンクを作成しています。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

ところでこのチュートリアル記事、めちゃくちゃ丁寧…w
公式より親切www

www.example.com/articles/index(indexなので、www.example.com/articlesでもOK)にアクセスすると以下が表示されます。

詳細ページ

上述のヘルパー機能によって生成したリンク先ページを作っていきます。
・コントローラーにviewアクションを追加
・リンク先ページviewを作成

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita
<?php
namespace App\Controller;

class ArticlesController extends AppController
{

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

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

今回は全件取得ではなく1件なのでget($id)を記述します。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita
<html>
<head></head>
<body>

<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>Created:<?= $article->created->format('y/m/d') ?></small></p>

</body>
</html>

記事の追加・編集・削除

・ArticlesController.phpに「add(), edit(), delete()」アクションを追加
・各アクションへのリンクをArticles/index.ctp内に記述

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

CRUD(Create, Read, Update, Delete)はどんなシステムでも基本中の基本。
これを実装していきます。

追加・編集・削除

完成ソース。

<?php

namespace App\Controller;

class ArticlesController extends AppController
{

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

    public function view($id = null)
    {
        $article = $this->Articles->get($id);
        $this->set(compact('article'));
    }
    
    public function add()
    {
        $article = $this->Articles->newEntity();
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());
            if ($this->Articles->save($article)) {
                $this->Flash->success(__('記事を保存しました。'));
                return $this->redirect(['action' => 'index']);
                $this->Flash->error(__('記事の追加が出来ません。'));
            }
        }
        $this->set('article', $article);
    }

    public function edit($id = null)
    {
        $article = $this->Articles->get($id);
        if($this->request->is(['post','put'])){
            $this->Articles->patchEntity($article, $this->request->getData());
            if($this->Articles->save($article)){
                $this->Flash->success(__('記事が更新されました'));
                return $this->redirect(['action' =>'index']);//ここの記述で保存実行時にindexページへ戻っている
            }
            $this->Flash->error(__('更新出来ませんでした。'));
        }
        $this->set('article', $article);
    }

    public function delete($id)
    {
        $this->request->allowMethod(['post', 'delete']);
        $article = $this->Articles->get($id);
        if($this->Articles->delete($article)){
            $this->Flash->success(__('id: {0} の記事が削除されました。', h($id)));
            return $this->redirect(['action' => 'index']);
        }
    }

}
<html>
<head></head>
<body>
<h1>ブログ記事</h1>
<?= $this->Html->link('記事追加',['action' => 'add']) ?>
<table>
    <tr>
        <th>Id</th>
        <th>タイトル</th>
        <th>作成日時</th>
        <th>編集</th>
    </tr>
    <?php foreach ($articles as $article) : ?>
        <tr>
            <td><?= $article->id ?></td>
            <td>
                <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>
            </td>
            <td>
                <?= $article->created->format('y/m/d'); ?>
            </td>
            <td>
              <?= $this->Form->postLink(
                  '削除 ',
                  ['action' => 'delete', $article->id],
                  ['confirm' => '本当に削除しますか?'])
              ?>
              <?= $this->Html->link('編集', ['action' => 'edit', $article->id]) ?>
          </td>
        </tr>
    <?php endforeach; ?>
</table>
</body>
</html>

追加

    public function add()
    {
        // ModelのArticlesで定義された空の変数(エンティティ)を$articleにコピー
        $article = $this->Articles->newEntity();

        // post送信かチェック
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());
            // 保存処理実行
            if ($this->Articles->save($article)) {
                // 保存処理が成功した場合の、フラッシュメッセージ
                $this->Flash->success(__('記事を保存しました。'));

                // www.example.com/articles/index へリダイレクト
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('記事の追加が出来ません。'));
        }
        $this->set('article', $article);
    }

・ユーザーがフォームを使ってデータを POST した場合、その情報は、 $this->request->getData()の中に入ってきます。
・FlashComponent の success() および error() メソッドを使って セッション変数にメッセージをセット。これらのメソッドは PHP の マジックメソッド を利用→レイアウトでは<?= $this->Flash->render() ?> を用いてメッセージを表示

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita
<html></html>
<head></head>
<body>
<h1>記事追加</h1>
<?php 

  echo $this->Form->create($article);
  <!-- <form method="post" action="/articles/add"> -->

  echo $this->Form->control('title');
  <!-- <div class="input text"> -->
  <!--     <label for="title">Title</label> -->
  <!--      <input type="text" name="title" maxlength="255" id="title"> -->
  <!-- </div> -->

  echo $this->Form->control('body');
  <!-- <div class="input textarea"> -->
  <!--     <label for="body">Body</label> -->
  <!--       <textarea name="body" id="body" rows="5"></textarea> -->
  <!-- </div> -->

  echo $this->Form->button(__('Save Article'));
  <!-- <button type="submit">Save Article</button> -->

  echo $this->Form->end();
 <!-- </form> -->
?>

作成されるコントロールの型は、カラムのデータ型に依存します。

Form – 3.10

$options パラメーターを使うと、必要な場合に特定のコントロールタイプを選択することができます。

<!-- 例 -->
echo $this->Form->control('published', ['type' => 'checkbox']);
Form – 3.10

www.example.com/articles/addにアクセス。

index.ctp記事追加のリンクを追加。

<html>
<head></head>
<body>
<h1>ブログ記事</h1>
<?= $this->Html->link('記事追加',['action' => 'add']) ?>
<table>
    <tr>
        <th>Id</th>
        <th>タイトル</th>
        <th>作成日時</th>
    </tr>
    <?php foreach ($articles as $article) : ?>
        <tr>
            <td><?= $article->id ?></td>
            <td>
                <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>
            </td>
            <td>
                <?= $article->created->format('y/m/d'); ?>
            </td>
        </tr>
    <?php endforeach; ?>
</table>
</body>
</html>

バリデーション作成

バリデーションのルールは、モデルの中で定義することができます

ArticlesTable.phpの既存クラス内にvalidationDefault()というアクションを追加します。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

ここで定義したバリデーションルールはControllerからArticlesTableのModelをsave() メソッドで呼んだときに使用されます。

use Cake\Validation\Validator; //このクラスの読み取り追加

//以下のメソッドを追加
 public function validationDefault(Validator $validator)
    {
        $validator
            // ->notEmpty('title') //コンソールで「'notEmpty' is deprecated.」とnotEmptyは非推奨ですと表示されるのでnotBlankを使用
            ->notBlank('title')
            ->requirePresence('title')
            ->notBlank('body')
            ->requirePresence('body');

        return $validator;
    }

他のバリデーションルールは公式を参考に。

編集

    public function edit($id = null)
    {
        // 記事IDを取得
        $article = $this->Articles->get($id);

        // post送信かつ、put(更新)メソッドかチェック
        if($this->request->is(['post','put'])){
            $this->Articles->patchEntity($article, $this->request->getData());
            if($this->Articles->save($article)){
                $this->Flash->success(__('記事が更新されました'));
                return $this->redirect(['action' =>'index']);//ここの記述で保存実行時にindexページへ戻っている
            }
            $this->Flash->error(__('更新出来ませんでした。'));
        }
        $this->set('article', $article);
    }

$this->request->is(['post','put'])にて、putメソッドが出ていて必須ではないけれど、まあちらっと頭の片隅にはあった方がいいと思うので、お時間があれば↓の記事をどうぞ。

しいていうなら、get($id)でidが取得できないことを想定して、patchEntityではなくnewEntityを使用してDBに保存するケースも想定すること、と先輩から教わりましたw

    public function edit($id = null)
    {
        $article = $this->Articles->get($id)

        if ($this->request->is('post')) {
            if (!emtpy($article)) {
                $article = $this->Articles->patchEntity($article, $this->request->getData());
            } else {
                $article = $this->Articles->newEntity($this->request->getData());
            }
            if ($this->Articles->save($article)) {
                $this->Flash->success(__('記事を保存しました。'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('記事の追加が出来ません。'));
            }
        }
        $this->set('article', $article);
    }

6行目のif文で$idが取得できていることを確認しています。

<html>
<head></head>
<body>
<h1>記事の編集</h1>
<?php 
  echo $this->Form->create($article);
  <!-- <form method="post" action="/articles/add"> -->

  echo $this->Form->control('title');
  <!-- <div class="input text"> -->
  <!--     <label for="title">Title</label> -->
  <!--      <input type="text" name="title" maxlength="255" id="title"> -->
  <!-- </div> -->

  echo $this->Form->control('body',['rows' =>'3']);
  <!-- <div class="input textarea"> -->
  <!--     <label for="body">Body</label> -->
  <!--       <textarea name="body" id="body" rows="5"></textarea> -->
  <!-- </div> -->

  echo $this->Form->button(__('記事保存'));
  <!-- <button type="submit">Save Article</button> -->

  echo $this->Form->end();
  <!-- </form> -->
?>
</body>
</html>
<html>
<head></head>
<body>
<h1>ブログ記事</h1>
<?= $this->Html->link('記事追加',['action' => 'add']) ?>
<table>
    <tr>
        <th>Id</th>
        <th>タイトル</th>
        <th>作成日時</th>
        <th>編集</th>
    </tr>
    <?php foreach ($articles as $article) : ?>
        <tr>
            <td><?= $article->id ?></td>
            <td>
                <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>
            </td>
            <td>
                <?= $article->created->format('y/m/d'); ?>
            </td>
            <td>
              <?= $this->Html->link('編集', ['action' => 'edit', $article->id]) ?>
          </td>
        </tr>
    <?php endforeach; ?>
</table>
</body>
</html>

削除

deleteはロジックを実行してリダイレクトする為、アクションに対してビューがありません。
こういうパターンがあることを抑える。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita
    public function delete($id)
    {
        $this->request->allowMethod(['post', 'delete']);
        $article = $this->Articles->get($id);
        if($this->Articles->delete($article)){
            $this->Flash->success(__('id: {0} の記事が削除されました。', h($id)));
            return $this->redirect(['action' => 'index']);
        }
    }
<html>
<head></head>
<body>
<h1>ブログ記事</h1>
<?= $this->Html->link('記事追加',['action' => 'add']) ?>
<table>
    <tr>
        <th>Id</th>
        <th>タイトル</th>
        <th>作成日時</th>
        <th>編集</th>
    </tr>
    <?php foreach ($articles as $article) : ?>
        <tr>
            <td><?= $article->id ?></td>
            <td>
                <?= $this->Html->link($article->title, ['action' => 'view', $article->id]) ?>
            </td>
            <td>
                <?= $article->created->format('y/m/d'); ?>
            </td>
            <td>
            <?= $this->Form->postLink(
                '削除 ',
                ['action' => 'delete', $article->id],
                ['confirm' => '本当に削除しますか?'])
            ?>
            <?= $this->Html->link('編集', ['action' => 'edit', $article->id]) ?>
          </td>
        </tr>
    <?php endforeach; ?>
</table>
</body>
</html>

デバッグ

実はこのチュートリアル、動きませんw
記事追加の画面で、Save Articleを押下すると以下のエラー画面が表示されます。

Error: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (DB名.articles, CONSTRAINT articles_ibfk_1 FOREIGN KEY (user_id) REFERENCES users (id))

articlesで外部キーが設定されているuser_idカラムで制約違反が発生してる、とのこと。
ブログ投稿で投稿者(user_id)が空な状況も想像できないので、外部キーが設定されているarticles.ueser_idとついでに同じエラーが発生するだろうarticles.slugを必須項目にします。

Model

    public function validationDefault(Validator $validator)
    {
        $validator
            ->notBlank('user_id')
            ->requirePresence('user_id')
            ->notBlank('slug')
            ->requirePresence('slug')

            // ->notEmpty('title') //コンソールで「'notEmpty' is deprecated.」とnotEmptyは非推奨ですと表示されるのでnotBlankを使用
            ->notBlank('title')
            ->requirePresence('title')
            ->notBlank('body')
            ->requirePresence('body');

        return $validator;
    }

Viwe

<html>
<head></head>
<body>

<h1>記事追加</h1>
<?php 
 echo $this->Form->create($article);
 echo $this->Form->control('user_id', ['type' => 'text']); // typeでtextを指定しないとプルダウンになる
 echo $this->Form->control('slug');
  
 echo $this->Form->control('title');
 echo $this->Form->control('body');
 echo $this->Form->button(__('Save Article'));
 echo $this->Form->end();
 ?>
 
</body>
</html>
<html>
<head></head>
<body>
<h1>記事の編集</h1>
<?php 
  echo $this->Form->create($article);
  echo $this->Form->control('user_id', ['type' => 'text']); //typeでtextを指定しないとプルダウンになる
  echo $this->Form->control('slug');

  echo $this->Form->control('title');
  echo $this->Form->control('body',['rows' =>'3']);
  echo $this->Form->button(__('記事保存'));
  echo $this->Form->end();
?>
</body>
</html>

記事を新規投稿します。

新規投稿できました。
ちなみにまだ直した方がいいところはありますが、これは最低限とは違うので今後の課題ですね。

ルーティング

www.example.comにアクセスすると、CakePHPのサンプルページが表示されます。
これは、config/routes.phpにそのルーティングの設定があるからです。

このルーティングを修正すると、www.example.comにアクセスしたときにwww.example.com/articles/indexを表示するように設定することが出来ます。

デフォルトで
$routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
となっている上記を
$routes->connect('/', ['controller' => 'Articles', 'action' => 'index']);
へ変更します。
これで、「/」でリクエストしてきたユーザーを、ArticlesController の index() アクションに 接続させることができます。

cakephp3 ブログチュートリアル解説 #cakephp3 – Qiita

‘home’ってなに?

homeは、Viewファイル名です。
function名とViewファイル名は自動的に関連づくので、どちらも同じ名前ならViewファイル名を省略できますが、function名とViewファイル名が違う場合はコードに書く必要があります。

$routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
// $routes->connect('/', ['controller' => controller名, 'action' => function名, Viewファイル名]);

今後の課題

user_idのプルダウン選択化

現状はuser_idは必須項目で、空白以外の文字が1文字以上、というバリデーションのみ。
このままだと存在しないuser_idも当たり前に登録できます。

なのでuser_idはusersテーブルからデータを取得してプルダウン選択に組み込むのがいいです。
が、それにはusersテーブルのデータを全件取得する処理が必要です。

user_idのCRUD

現状usersテーブルには1人しか登録されておらず、新規作成・表示・編集・削除にはDBを直接見るしか方法がないです。
これだと不便なのでusersテーブルを操作する画面が必要かなと思います。

slugの重複チェック

articles.slugはユニークキーなんですよね。
つまり、同じslugで保存されようとしたときはバリデーションで弾いた方がいいです。

ぐぐればすぐに出てくるけど、このバリデーションは公式サイトに書いていないので、これも最低限とは違うかなと。

まとめ

チュートリアルは以上です。
個人的にはクエリビルダーが鬼門だと思ってるので、今度はそれ関係の記事を書こうかな。