CakePHP3・4初心者向け。ネットでは見つからないちょっとした実装集

CakePHP3・4初心者向け。ネットでは見つからないちょっとした実装集

345 回閲覧されました

みなさんこんにちは、jonioです。

CakePHPに関して検索しても見つけることができなくて苦労して実装した実装集を掲載します。

情報が見つかり次第追記します。

おすすめの参考書

CakePHPの学習でおすすめの参考書を紹介します。

悪い口コミもありますが分からない部分はChatGTPに質問すればいいので問題ないと思います。

アソシエーション時に条件を満たしたら削除ボタンを表示

アイドル会社1とアイドル会社2があってそれぞれの会社に部署1と部署2があり部署1と部署2にはそれぞれ従業員が3人いたとします。

会社のカラムは下記になります。

部署のカラムは下記になります。

従業員のカラムは下記になります。

会社はcompaniesテーブル、部署はdepartmentsテーブル、従業員はusersテーブルに情報が保存されているとします。

アソシエーションの関係は以下になります。

  • 従業員(usersテーブル) :companiesテーブルとdepartmentsテーブルに対して子テーブル
  • 部署(departmentsテーブル) : companiesテーブルに対して子テーブル
  • 会社(companiesテーブル) : departmentsテーブルとusersテーブルに対して親テーブル

アソシエーションの設定はしたとします。

viewアクション

「http://localhost/companies/view/1」にアクセスした時に下記の表示にするのを目標にします。

会社の中で部署に紐づいているユーザーがいますが部署に紐づくユーザーがいなくなったら削除ボタンが表示されるようにします。

Companiesコントローラーのviewアクションのコードは下記になります。

public function view($id = null)
{
  $company = $this->Companies->get($id, [
    'contain' => ['Users'],
  ]);

  $companyId = $this->Auth->user('company_id');

  $departments = $this->Companies->Departments->find()
                                              ->where(['company_id' => $companyId])
                                              ->contain(['Users'])
                                              ->all();

  $this->set(compact('company', 'departments'));
}

今回の実装に関係あるのは7行目〜12行目です。

会社に紐づく部署を考えるので9行目の「->Companies->Departments」があります。

実装に使う会社はユーザーがログインしている会社にしないといけないので会社idを7行目で取得して10行目で使っています。

これだけだとユーザーが考慮されていないのでユーザーも含めます。

ユーザーがログインしている会社の中のユーザーを使うので11行目があります。

companiesテーブルに紐づくdepartmentsテーブルに紐づくユーザーが取得できているかを確認します。

viewアクションの中に下記の記述をします。

public function view($id = null)
{
  $company = $this->Companies->get($id, [
    'contain' => ['Users'],
  ]);

  $companyId = $this->Auth->user('company_id');

  $departments = $this->Companies->Departments->find()
                                              ->where(['company_id' => $companyId])
                                              ->contain(['Users'])
                                              ->all();

  dd($departments);        //←この行を追加
  
  $this->set(compact('company', 'departments'));
}

すると会社の部署に紐づいているユーザーがいる場合は下記の表示になります。

会社の部署に紐づいているユーザーがいない場合は下記の表示になります。

viewアクションで表示するビューのコードを下記にします。

<div class="related">
  <h4><?= __('部署') ?></h4>
  <div class="table-responsive">
    <table>
       <tr>
         <th><?= __('Name') ?></th>
         <th class="actions"><?= __('Actions') ?></th>
       </tr>
       <?php foreach ($departments as $department) : ?>
         <tr>
           <td><?= h($department->name) ?></td>
           <td class="actions">
             <?= $this->Html->link(__('View'), ['controller' => 'Departments', 'action' => 'view', $department->id]) ?>
             <?= $this->Html->link(__('Edit'), ['controller' => 'Departments', 'action' => 'edit', $department->id]) ?>
             <?php if(count($department->users) == 0): ?>
               <?= $this->Form->postLink(__('Delete'), ['controller' => 'Departments', 'action' => 'delete', $department->id], ['confirm' => __('Are you sure you want to delete # {0}?', $department->id)]) ?>
             <?php endif; ?>
           </td>
         </tr>
       <?php endforeach; ?>
     </table>
   </div>
</div>

部署に紐づく従業員が0人の時のみ削除ボタンを表示させているのが15行目です。

viewアクションで「dd($departments);」をして部署に紐づくユーザーを取得するのは「$department->users」です。(usersは下記参照)

これで実装が完成です。

jQuery(JavaScript)にコントローラー側からの変数を含める

CakePHP3.1で実装しています。

コントローラーのアクションに下記の記述をしたとします、ajaxを使おうとしています。

$this->set('_serialize', ['names', 'departments']);

namesには名前が入っていてdepartmentsには部署が入っていたとします。

そしてjQueryに下記の記述をしたとします。(必要な部分のに記載しています)

$(document).ready(function() {

  function loadCards() {

    $.ajax({
                
      url: '/users/index/',

      data: { page: page },

      dataType: 'json'
    })
    .done(function(response) {
           
    response.departments.forEach(department => {
              
      let department_content = `
        <div class="department-wrap">${department.content}</div>
      `;

      if (response.names == 'jonio') {
        department_content += `
          <p>2024年</p>
        `;
      }



アクションのnamesをjQueryの21行目で使っています。

存在しないURLにアクセスすると特定のページにリダイレクト

CakePHP4.5.4で実装しました、3系では動かないかもしれません。

ErrorController.php

「CakePHPのプロジェクト > src > Controller > ErrorController.php」の「beforeRender」メソッドに記述します。

public function beforeRender(EventInterface $event)
{
  parent::beforeRender($event);

  if ($this->response->getStatusCode() == 404) {

    return $this->redirect('リダイレクトする時のコントローラーとアクションを指定');
  }
}

beforeRenderメソッドはビューを表示する前に処理をする為の記述をする時に使うみたいです。

だからリダイレクトの処理はこのメソッドに記述します。

リダイレクト時のフラッシュの表示

コードを下記にします。

<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.3.4
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App\Controller;

use Cake\Event\EventInterface;

/**
 * Error Handling Controller
 *
 * Controller used by ExceptionRenderer to render error responses.
 */
class ErrorController extends AppController
{
    /**
     * Initialization hook method.
     *
     * @return void
     */
    public function initialize(): void
    {
        $this->loadComponent('RequestHandler');

        $this->loadComponent('Flash');    //この行を追加
    }
    



                  
    /**
     * beforeRender callback.
     *
     * @param \Cake\Event\EventInterface $event Event.
     * @return \Cake\Http\Response|null|void
     */
    public function beforeRender(EventInterface $event)
    {
        parent::beforeRender($event);

        if ($this->response->getStatusCode() == 404) {

            $this->Flash->error(__('アクセスしたURLは存在しません'));    //この行を追加

            return $this->redirect('/');
        }
    }
}

通常のコントローラーだと「$this->loadComponent(‘Flash’);」の記述があるAppControllerを継承しているのでいきなり56行目の記述ができますがErrorControllerはAppControllerを継承しても「$this->loadComponent(‘Flash’);」が適用されないみたいです。

だからinitializeメソッドに記述(37行目)しています。

これで404の時に特定のページにリダイレクトしてフラッシュメッセージも表示されます。

ログインなしでアクションを許可している時に動作しない

何らかのコントローラーで下記の記述をすればログインしなくてもアクションの動作ができるようになります。

use Cake\Event\EventInterface;




public function beforeFilter(EventInterface $event)
{
  parent::beforeFilter($event);
        
  $this->Authentication->addUnauthenticatedActions(['add']);
}

この場合はaddアクションが対象になります。

アクションを修正した時に起こる可能性がありますがアクションに下記などのログイン情報を記述していたとします。

$this->request->getAttribute('identity');

cookbookに記載がなくネットで情報を見つけることができなかったのですがアクションに対してログインを必要なくしてもアクションにログイン関連の記述をするとログインが必須になる仕様みたいです。

特定のデバイスでページにアクセスできなくする

例えばログインページにはリンクからアクセスしてもURLからアクセスしてもスマホとタブレットからしかアクセスできなくしたかったとします。

AppController.php

コードに下記を追記します。

protected function isMobileOrTablet()
{
  $user_device = $this->request->getEnv('HTTP_USER_AGENT');

  $target_devices = ['Android', 'iPhone', 'iPad', 'iPod', 'BlackBerry', 'Opera Mini', 'IEMobile'];

  foreach ($target_devices as $target_device) {
    if (strpos($user_device, $target_device) == true) {
      return true;
    }
  }

  return false;
}

3行目でアクセスしようとしている人の端末をチェックしています。

5行目でアクセスを可能にする端末を指定しています、これだけあれば十分だと思います。

7行目〜11行目で自分の端末がアクセス可能の端末に当てはまっているかをチェックしています。

次はコントローラーです。

loginアクションがあるコントローラー

下記のコードを追記します。

public function login()
{


  //ここから追加
  if (!$this->isMobileOrTablet()) {
    return $this->redirect('/');
  }
  //ここまで追加
  

  if ($this->request->is('post')) {
    $user = $this->Auth->identify();
    if ($user) {
      $this->Auth->setUser($user);
      return $this->redirect(['action' => 'index']);
    }
    $this->Flash->error(__('Invalid username or password, try again'));
  }
}

今回はアクセス禁止の端末の場合にトップページにリダイレクトさせていますがお好みで変えてください。

ユーザーを登録する際にランダムな文字列を生成

場合によってはユーザー登録の際にパスワードとしてランダムな文字列を生成して登録ユーザーにメールでパスワードを送ってユーザーしかパスワードが分からない状態にしたいかもしれません。

そんな時に有効だと思います。

Cakephp3.6で動作の確認をしています。

UsersController.php

コードに下記の追記をします。

use Cake\Utility\Security;          //この行を追加





            

public function add()
{
  $user = $this->Users->newEntity();
  if ($this->request->is('post')) {


    //ここから追加
    $plain_password = bin2hex(Security::randomBytes(6));

    $this->log('Generated Password: ' . $plain_password, 'debug');
    //ここまで追加
    

    $user = $this->Users->patchEntity($user, $this->request->getData());

    $user->password=$plain_password;        //この行を追加

    if ($this->Users->save($user)) {
      $this->Flash->success(__('The user has been saved.'));

      return $this->redirect(['action' => 'index']);
    }
    $this->Flash->error(__('The user could not be saved. Please, try again.'));
  }
  $this->set(compact('user'));
}

18行目はパスワードを確認するために付けていますが本番の環境でパスワードをログに出力すると丸わかりになるので付けるべきではありません。

16行目がポイントです。

「Security::randomBytes(6)」で安全な6バイトのバイナリデータ(パソコンしか認識できない文字列)に変換します。

これではパスワードが分からないので人間でも分かるパスワードに変換する為に「bin2hex」を付けてパスワードを16進数に変換します。

10進数とか他の進数でもいいですがパスワードが長くなったり短くなったりするとパスワードとして適さないのでパスワードとして適する12文字にする為に16進数に変換します。

ビュー

パスワードは登録しないので下記のコードにします。

<div class="users form large-9 medium-8 columns content">
    <?= $this->Form->create($user) ?>
    <fieldset>
        <legend><?= __('Add User') ?></legend>
        <?php
            echo $this->Form->control('name');
            echo $this->Form->control('email');
        ?>
    </fieldset>
    <?= $this->Form->button(__('Submit')) ?>
    <?= $this->Form->end() ?>
</div>

データ型をTinyIntにする

マイグレーションファイルに下記の記述をすればint型からTinyInt型に変えることができます。

<?php
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;        //この行を追加

class Createxxxxxxxxxxxx extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('xxxxxx');
        $table
        ->addColumn('stamp_password_check', 'integer', [
            'limit' => MysqlAdapter::INT_TINY              //この行を追加
        ])
        ->create();
    }
}

登録フォームで姓と名に分けた名前の値を1つのカラムに登録

下記画像のように姓と名の2つに別れたフォームの値をつなげてテーブルの1つのカラムに登録したい場合があります。

そんな時のコードの書き方です。

テンプレート

まずは登録フォームです。

下記のコードを書いたとします。

<?= $this->Form->create($demo, ['url' => ['action' => 'edit']]) ?>
  <div>
    <p>お名前</p>
    <div>
      <?= $this->Form->control('last_name', [
        'label' => false,
        'placeholder' => '姓',
        'name' => 'last_name'
      ]) ?>
    </div>
    <div class="half-width">
      <?= $this->Form->control('first_name', [
        'label' => false,
        'placeholder' => '名',
        'name' => 'responsible_last_name'
      ]) ?>
    </div>
  </div>
  <?= $this->Form->button('登録') ?>
<?= $this->Form->end() ?>

8行目と9行目でname属性を設定しています。

コントローラー

コードを下記にします。

public function アクション名()
{



  if ($this->getRequest()->is(['patch', 'post', 'put'])) {
    $formData = $this->getRequest()->getData();

    $user = $this->Users->patchEntity($user, $formData);

    $formData['name'] = $formData['first_name'] . '/' . $formData['last_name'];
    $user->name = $formData['name'];



}

このやり方で登録するとテーブルに保存される値は「姓のフォームに入力した値/名のフォームに入力した値」になります。

姓と名の間に「/」がありますがこれは姓と名を表示する時に使うからです。

姓と名の表示

コントローラーのコードを下記にします。

アクションはテンプレートへの表示用を使っています。

public function アクション名()
{
  $userInfo = $this->Users->get();

  $name = explode("/", $userInfo->name);



  $this->set(compact('name'));
}

そしてテンプレートのコードは下記にします。

<?= $this->Form->create($demo, ['url' => ['action' => 'edit']]) ?>
  <div>
    <p>お名前</p>
    <div>
      <?= $this->Form->control('last_name', [
        'label' => false,
        'placeholder' => '姓',
        'value' => $name[0],      //この行を追加
        'name' => 'last_name'
      ]) ?>
    </div>
    <div class="half-width">
      <?= $this->Form->control('first_name', [
        'label' => false,
        'placeholder' => '名',
        'value' => $name[1],      //この行を追加
        'name' => 'responsible_last_name'
      ]) ?>
    </div>
  </div>
  <?= $this->Form->button('登録') ?>
<?= $this->Form->end() ?>

これでnameカラムの値を姓と名に分けてフォームの中に表示できます。

1つのフォームに複数のsubmit

1つのフォームの中に

ネットに少ないながらも情報はありますが全てうまくいきませんでした。

業務で上司に質問して「動作しない理由が分からない」とのことで無理矢理動くようにしました。

ネットの情報

下記のコードで押したボタンのvalue属性が表示されると説明がありました。

//テンプレート
<?= $this->Form->create(null, ['url' => ['action' => 'demo']] )?>
  <?= $this->Form->button('寝る', ['type' => 'submit', 'class' => 'btn btn-danger', 'name' => 'action', 'value' => 'sleepAction']) ?>
  <?= $this->Form->button('働く', ['type' => 'submit', 'class' => 'btn btn-info',  'name' => 'action', 'value' => 'workAction']) ?>
  <?= $this->Form->button('勉強', ['type' => 'submit', 'class' => 'btn btn-primary', 'name' => 'action', 'value' => 'studyAction']) ?>
<?= $this->Form->end() ?>


//コントローラー
if ($this->request()->is('post')) {
  dd($this->getRequest()->getData());
  exit;
}


//結果
[
	'action' => 'work'
]

しかしうまくいきませんでした。

対応

テンプレートのコードを下記にします。

<?= $this->Form->create(null, ['url' => ['action' => 'demo']] )?>
  <?= $this->Form->hidden('action', ['id' => 'actionType']) ?>       //この行をつける
  <?= $this->Form->button('寝る', ['type' => 'button', 'class' => 'btn btn-danger action', 'id' => 'sleep', 'name' => 'action', 'value' => 'sleepAction']) ?> 
  <?= $this->Form->button('働く', ['type' => 'button', 'class' => 'btn btn-info action', 'id' => 'work',  'name' => 'action', 'value' => 'workAction']) ?> 
  <?= $this->Form->button('勉強', ['type' => 'button', 'class' => 'btn btn-primary action', 'id' => 'study', 'name' => 'action', 'value' => 'studyAction']) ?> 
<?= $this->Form->end() ?>

2行目を追記します。

そしてtype属性はsubmitだったのをbuttonに変更してクラス名にactionを追記します。

そしてjQueryに下記のコードを書きます。

<script>
    (function($) {
        $('.action').on('click', function() {
            var text = $(this).val();
            $('#actionType').val(text);
        
            $('form').submit();
        });
    })(jQuery);
</script>

これで寝る・働く・勉強ボタンを押した時にvalue属性が表示されます。

//コントローラー
if ($this->request()->is('post')) {
  dd($this->getRequest()->getData());
  exit;
}


//「寝る」ボタンを押した場合の結果
[
	'action' => 'sleepAction'
]

検索をした時にURLからパラメーターを消す

テンプレートに下記の記述をしたとします。

<?= $this->Form->create(null, ['type' => 'get', 'url' => ['action' => 'index']]) ?>
    <div>
        <?= $this->Form->radio('search_type', [
            ['value' => 'name', 'text' => 'ユーザー名'],
            ['value' => 'name_kana', 'text' => 'ふりがな']
        ]) ?>
        <?= $this->Form->control('keyword', ['placeholder' => '検索キーワードを入力', 'label' => false]) ?>
        <?= $this->Form->button('検索', ['type' => 'submit']) ?>
    </div>
<?= $this->Form->end() ?>

コントローラーに下記の記述をしたとします。

$users = $this->User->find();

$searchType = $this->getRequest()->getQuery('search_type');
$keyword = $this->getRequest()->getQuery('keyword');

if (!empty($keyword)) {
    switch ($searchType) {
        case 'name':
            $users->where(['Users.name LIKE' => '%' . $keyword . '%']);
            break;
        case 'name_kana':
            $users->where(['Users.name_kana LIKE' => '%' . $keyword . '%']);
            break;
    }
}

$users = $this->paginate($users);

これで検索すると検索語のURLが下記になります。

http://localhost/users?search_type=&search_type=name&keyword=a

検索結果のURLに「?search_type=&search_type=name&keyword=a」が残って相当使いにくいです。

jQueryで対応します。

jQuery

if (window.history.replaceState) {
    let url = new URL(window.location);
    url.searchParams.delete('search_type');
    url.searchParams.delete('keyword');
    window.history.replaceState(null, null, url.pathname);
}

これで検索後のURLから「?search_type=&search_type=name&keyword=a」を消すことができます。

5行目の「url.pathname」で「http://localhost/users」の「/users」を取得して「http://localhost」 + 「/users」で元々のURLになります。

Date型のフォームを編集する方法

テンプレートで下記のコードを書いたとします。

<?php echo $this->Form->control('register_date', ['label' => '登録日', 'type' => 'date', 'dateFormat' => 'YMD', 'id' => 'register-date']); ?>

この記述をするとブラウザには下記の表示がされます。

テーブルに保存した登録日を編集する時にこの値をそのまま使うことはできません。

編集フォームをjQeuryで表示させて登録日の編集をする方法を載せます。

jQuery

編集するのは年・月・日なのでテーブルに登録しているデータ(2024-09-08 T 05:19:15みたいなデータ)を年・月・日に分割して編集するフォームの年・月・日に代入します。

jQueryのコードを書きます。

let register_date_value = user.register_date.split('T')[0].split('-');

$('select[name="register_date[year]"]').val(register_date_value[0]);
$('select[name="register_date[month]"]').val(register_date_value[1]);
$('select[name="register_date[day]"]').val(register_date_value[2]);

1行目で「2024-09-09 T 05:19:15」のT以降をなくして2024-09-08の「-」を切って「2024」・「09」・「08」を配列の要素として格納します。

そして3行目で年を設定して4行目で月を設定して5行目で日を設定しています。

これでうまくいきます。

リダイレクトにパラメーターを含める

下記のようなURLにリダイレクトしたかったとします。

http://localhost:8888/demo?parameter=14

この時の「?parameter=14」をコントローラーで使う方法です。

テンプレート

下記の記述をします。

<?= $this->Form->create(null, ['type' => 'post', 'url' => ['action' => '']]) ?>
    <?= $this->Form->hidden('checked', ['value' => $check->id]) ?>
    
    //登録や編集のフォーム
    
    <?= $this->Form->button('登録', ['type' => 'submit']) ?>
<?= $this->Form->end() ?>

2行目の「$check->id」をコントローラーに渡します。

コントローラー

下記の記述をします。

public function()
{
  $parameter_value = $this->getRequest()->getData('checked');
  



   
  return $this->redirect(['action' => '', '?' => ['parameter' => $parameter_value]]);
}

これでうまくいきます。

月に対応したデータの出力

セレクトボックスを切り替えることで月に対応したユーザーの名前を表示させます。

セレクトボックスの切り替え

コントローラーのコードを下記にします。

public function workList()
{
    $months = [];

    for ($i = 0; $i < 12; $i++) {
        $timestamp = strtotime("-$i month");
        
        $months[date('Y-m', $timestamp)] = date('Y年n月', $timestamp);
    }

    $selected_month = $this->request->getQuery('month');

    $startDate = new \DateTime("$selectedMonth-01");
    
    $endDate = (clone $startDate)->modify('last day of this month')->setTime(23, 59, 59);

    $this->set(compact('months', 'selected_month');
}

今回は現在からさかのぼって1年分のデータを取得できるようにします。

3行目はセレクトボックスの初期値を空っぽに設定しています。

テンプレートでセレクトボックスを表示する為にこれに値を設定するのが5行目〜9行目です。

1年分を5行目の12で設定しています。

6行目で月をさかのぼることができるようにしています。

8行目はセレクトボックスの値の設定です。

「$months[date(‘Y-m’, $timestamp)]」はkeyで「date(‘Y年n月’, $timestamp)」はvalueです。

「date(‘Y-m’, $timestamp)」は$timestampをY-mの形式で表示するという意味です。

11行目はテンプレートでの選択した月を取得しています。

13行目は月の最初の日を取得して15行目は月の最後の日の23時59分59秒を取得します。

次はテンプレートで月の切り替えができるようにします。

月の切り替え

テンプレートのコードを下記にします。

<form method="get">
    <label for="month-select">処理月:</label>
    <select name="month" id="month-select" onchange="this.form.submit()">
        <?php foreach ($months as $value => $label): ?>
            <option value="<?= h($value) ?>" <?= $selectedMonth == $value ? 'selected' : '' ?>><?= h($label) ?></option>
        <?php endforeach; ?>
    </select>
</form>

5行目の「<?= $selectedMonth == $value ? ‘selected’ : ” ?>」でセレクトボックスで月を選択すると選択した状態になります。

次はセレクトボックスで月を選択した時に選択した月に対応するユーザー情報を表示できるようにします。

月に対応するユーザー情報の取得

コントローラーのコードを下記にします。

public function workList()
{
    $months = [];

    for ($i = 0; $i < 12; $i++) {
        $timestamp = strtotime("-$i month");
        
        $months[date('Y-m', $timestamp)] = date('Y年n月', $timestamp);
    }

    $selected_month = $this->request->getQuery('month');

    $startDate = new \DateTime("$selectedMonth-01");
    
    $endDate = (clone $startDate)->modify('last day of this month')->setTime(23, 59, 59);
    
    
    //ここから追加
    $users = $this->Users->find()->where([
      'stamp_date >=' => $startDate->format('Y-m-d H:i:s'),
      'stamp_date <=' => $endDate->format('Y-m-d H:i:s')
    ]);
    //ここまで追加


    $this->set(compact('months', 'selected_month', 'users');      //この行を修正
}

次はテンプレートでユーザーの情報を表示します。

ユーザーの表示

テンプレートのコードを下記にします。

<form method="get">
    <label for="month-select">処理月:</label>
    <select name="month" id="month-select" onchange="this.form.submit()">
        <?php foreach ($months as $value => $label): ?>
            <option value="<?= h($value) ?>" <?= $selectedMonth == $value ? 'selected' : '' ?>><?= h($label) ?></option>
        <?php endforeach; ?>
    </select>
</form>

<?php foreach ($users as $user): ?>
  <?= $user->name ?>
<?php endforeach ?>

バリデーションエラーの内容を表示

コントローラーのaddアクションに下記の記述をしたとします。

public function add()
{
    $user = $this->Users->newEntity();

    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->getData());

        if ($user->getErrors()) {
            debug($user->getErrors());
            exit;
        }

        if ($this->Users->save($user)) {
            $this->Flash->success(__('ユーザーが保存されました。'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('ユーザーの保存に失敗しました。もう一度お試しください。'));
    }

    $this->set(compact('user'));
}

8行目〜11行目がバリデーションエラーを表示する為の記述です。

バリデーションエラーの設定はサンプルコードの場合はUsersTable.phpで行います。

public function validationDefault(Validator $validator)
{
    $validator
        ->notEmptyString('email', 'メールアドレスを入力してください')
        ->email('email', false, '正しいメールアドレスを入力してください');

    return $validator;
}