Lesson 10

ヘルパー・コンポーネント・ビヘイビア

Lesson 10 Chapter 1
ヘルパー

Lesson10では主にビュー・コントローラ・モデルに対して特定の機能を追加をする際に使用される「ヘルパー・コンポーネント・ビヘイビア」というものについて見て行きます。これらはクラスとメソッドから成っており、主にヘルパーはビュー、コンポーネントはコントローラ、ビヘイビアはモデルのクラス内で必要に応じて呼び出し使用することができます。

10_1_1.png

また、上のイメージ図ではCakePHPがもともと用意しているヘルパー・コンポーネント・ビヘイビアを呼び出し使用することを想定していますが、いずれも自作することが可能なため、頻繁に使う機能をまとめておくことでより柔軟な開発を行うことが可能となります。

早速ヘルパーから順にそれぞれどのような機能を持っているのか具体的に見ていきます。

Html ヘルパー

最初に紹介する Htmlヘルパーは各種リンクやテーブルなどの表示に必要なHTMLを適切に成形して返す機能を豊富に揃えています。

使い方は、ビューテンプレート内において、「$this->Html」とすることで Htmlヘルパークラスのオブジェクトを呼び出し、各種メソッドを使用する形となります。以下で主要なメソッドの使い方を見ていきましょう。

CSSの読み込み

最初はcssファイルを読み込むためのリンクを成形して出力するメソッドです。

<使用例>
<?= $this->Html->css('main'); ?>

<出力結果>
<link rel="stylesheet" href="/css/main.css">

上記のように、$this->Htmlからcssメソッドを呼び出すことで簡単に、cssファイルを読み込むlinkタグを生成することができます。指定された CSS ファイルは webroot/css ディレクトリー内にあることを前提としています。

読み込むcssファイルが複数ある場合には第一引数に配列で指定することで、2つ以上のlinkタグを生成することができます。

また、生成したlinkタグはデフォルトではbodyタグ内に入りますが、headタグ内でcssファイルの読み込みをまとめて行う場合には、第二引数にblockオプション追加し、true としておくことでcssのlinkタグをheadタグ内のcssブロックへ自動的に入れることが出来ます。

<使用例>
<?= $this->Html->css(['style','base'],['block' => true]); ?>

<出力結果>
// blockオプションにより自動でheadタグ内に生成される。
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/base.css">

ファイルの読み込みではなく、styleタグを使って cssをファイル内にインラインで記述する場合は style()メソッドを使うことが出来ます。

<使用例>
<style>
  h1 {
  <?php echo $this->Html->style([
    'color' => '#466',
    'background' => 'white',
    'border-bottom' => '3px solid #999',
    'padding' => '10px'
  ]); ?>
  }
</style>

<出力結果>
<style>
  h1 {
  color:#466; background:white; border-bottom:3px solid #999; padding:10px;  }
</style>

style()メソッドを使った記述では、cssのプロパティの値に変数を使用する場合などに特に有用です。

javascriptの読み込み

$this->Html->script()を使用することで、javascriptファイルの読み込みを行うscriptタグの生成が可能です。

<使用例>
<?= $this->Html->script('main',['block' => true]); ?>

<出力結果>
<script src="/js/main.js"></script>

<使用例>
//アプリケーション外部のURLを指定することも可能。
<?= $this->Html->script('https://code.jquery.com/jquery.min.js',['block' => true]); ?>

<出力結果>
<script src="https://code.jquery.com/jquery.min.js"></script>

css()メソッドと同様に、第二引数でblockオプションを true にすることで headタグ内に含めることができます。
加えて、第一引数に配列を渡すことで複数のファイルを指定することも可能です。

また、HTMLの読み込み後にjavascriptを実行させたい場合には、deferをオプションに入れるようにします。以下はscriptタグをインラインで生成するためのscriptBlockメソッドを使用し、deferのオプションを追加している例となります。

<使用例>
<?= $this->Html->scriptBlock('alert("アラート")', ['defer' => true]); ?>

<出力結果>
//deferにより、HTML読み込み後に実行される。
<script defer="defer">alert("アラート")</script>

scriptの記述が長くなる場合などには、scriptStart()とscriptEnd()メソッドを使ってscriptタグの生成を行うことも可能です。

<使用例>
<?= $this->Html->scriptStart(['block' => true]); ?>
<?= "alert('アラート');"; ?>
<?= $this->Html->scriptEnd(); ?>

<出力結果>
<script>alert('アラート');</script>

画像のリンク

成形されたimgタグの生成にはimage()メソッドを使用し、以下のように記述します。画像ファイルの参照先はwebroot/img の配下となります。

<使用例>
<?= $this->Html->image('logo.png', ['alt' => 'logo']); ?>

<出力結果>
<img src="/img/logo.png" alt="logo" />

画像にリンクをつける場合は第二引数にurlオプションを追加し、以下のように記述することが出来ます。

<使用例>
<?= $this->Html->image("profile/1.jpg", [
    "alt" => "profile_1",
    'url' => ['controller' => 'Users', 'action' => 'profile', 1]
]); ?>

<出力結果>
<a href="/Users/profile/1">
    <img src="/img/profile/1.jpg" alt="profile_1" />
</a>

また、メール内や外部サイトなど、自サイト以外において画像への絶対パスを指定する必要がある場合には、以下のようにfullBaseをオプションを指定します。

<使用例>
<?= $this->Html->image("logo.png", ['alt' => 'logo'], ['fullBase' => true]);?>

<出力結果>
<img src="https://sample.com/img/logo.png" alt="logo">

リンクの作成

Html->link()を使って様々なaタグを作成することが出来ます。第一引数にリンクのテキスト、第二引数にリンク先URL、第三引数に属性などのオプションを取ります。

$this->Html->link(リンクテキスト, url, 配列でのオプション)

以下の例ではURLを直接指定しています。

<使用例>
<?= $this->Html->link('リンク', '/users/add'); ?>

<出力結果>
<a href="/users/add">リンク</a>

リンク

以下はコントローラとアクションでのURL指定と、属性にクラスとターゲットの追加をした形です。

<使用例>
<?= $this->Html->link('リンク', ['controller' => 'users', 'action' => 'add'],['class' => 'button', 'target' => '_blank']); ?>

<出力結果>
<a href="/users/add" class="button" target="_blank">リンク</a>


リンク

絶対パスにする場合には、オプションに「_full=>true」を追加します。

<使用例>
<?= $this->Html->link('リンク', ['controller' => 'users', 'action' => 'add', '_full' => true]); ?>

<出力結果>
<a href="http://localhost:8765/users/add">リンク</a>

リンク

クエリー文字列でのURL指定も可能です。

<?= $this->Html->link('リンク', ['controller' => 'tasks', 'action' => 'detail', '?' => ['id' => '1']]); ?>

//出力結果
<a href="/tasks/detail?id=1">リンク</a>

リンク

リストの作成

ul,olでのリストを作成する場合にはnestedList()を使用します。第一引数に表示するデータとなる配列を渡します。デフォルトではulタグを生成し、typeをolにする場合はオプションで指定することが可能です。

<使用例>
//配列データを用意
<?php
  $list = [
    'A',
    'B' => [
      '1',
      '2'
    ],
    'C' => [
      '1',
      '2',
      '3' => [
        'a'
      ]
    ]
  ];
?>
//nestedList()に配列データを渡す。
<?= $this->Html->nestedList($list); ?>


<出力結果>
<ul>
  <li>A</li>
  <li>B
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
  </li>
  <li>C
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3
        <ul>
          <li>a</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

  • A
  • B
    • 1
    • 2
  • C
    • 1
    • 2
    • 3
      • a

olタグを使用する場合はオプションとしてtagをolに指定。

<使用例>
<?= $this->Html->nestedList(['A','B','C'], ['tag' => 'ol']); ?>

<出力結果>
<ol>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ol>

  1. A
  2. B
  3. C

テーブルの作成

テーブルの作成にはtableタグのブロック内でtableHeaders()とtableCelles()メソッドを使用することが出来ます。

//ヘッダーの生成
$this->Html->tableHeaders(配列,<tr>の属性,<th>の属性)
//セルの生成
$this->Html->tableCelles(配列,奇数行の属性,偶数行の属性);
<使用例>
<table>
  <?= $this->Html->tableHeaders([
    'id',
    'title',
    'Date'
  ], ['style'=>'color:#333; background-color:#fff']); ?>
  
  <?= $this->Html->tableCells(
    [
      ['1', 'A', '2020/01/30'],
      ['2', 'B', '2022/06/20'],
      ['3', 'C', '2023/12/19' ]
    ],
    ['style'=>'color:#333; background-color:#aaa'],
    ['style'=>'color:#333; background-color:#fff']
  ); ?>
</table>

<出力結果>
  <table>
    <tbody>
      <tr style="color:#333; background-color:#fff">
        <th>id</th> <th>title</th> <th>Date</th>
      </tr>
      <tr style="color:#333; background-color:#aaa">
        <td>1</td> <td>A</td> <td>2020/01/30</td>
      </tr>
      <tr style="color:#333; background-color:#fff">
        <td>2</td> <td>B</td> <td>2022/06/20</td>
      </tr>
      <tr style="color:#333; background-color:#aaa">
        <td>3</td> <td>C</td> <td>2023/12/19</td>
      </tr>
    </tbody>
  </table>

  
id title Date
1 A 2020/01/30
2 B 2022/06/20
3 C 2023/12/19

上記の例では属性設定でbackground-colorを指定し、行ごとの色分けを行っています。

Form ヘルパー

Formヘルパーにはフォームの開始と終了タグ、各種フォーム要素の作成を行うメソッドなどが用意されています。

特徴として、フォームの開始時にエンティティオブジェクトのデータを渡すことで、エンティティのもつフィールドやバリデーションのルールが読み込まれ、データをエンティティに送るための適切なフォームを簡単に生成することが出来る点が挙げられます。

また、Formヘルパーを使って作成されたフォームには「CSRFトークン」が自動的に付与されるため、セキュリティ上の対策ともなります。

以下、Formヘルパーの持つメソッドの使い方を説明していきます。

createとend

フォームの開始にはcreate()メソッドを使用します。第一引数にエンティティオブジェクトを取ることで、特定のモデルのエンティティに基づくフォームを作ることができます。nullを入れ、エンティティを取らずにフォームをつくることも可能です。

第二引数にはオプションとして、送信先のURLや送信メソッドのタイプを指定することができます。指定しない場合には現在のページへpostで送信を行います。

  • モデル指定なし、users/addへpost送信を行う
    <使用例>
    <?= $this->Form->create(null, ['url' => ['controller' => 'Users', 'action' => 'add'], 'method' => 'post']); ?>
    
    <出力結果>
    <form method="post" action="/users/add">
  • エンティティーを引数に取ってフォームを作成
    <使用例>
    //UsersControllerのaddアクションのビュー内で
    <?= $this->Form->create($user); ?>
    
    <出力結果>
    //送信先を省略しているので、送信元のusers/addへと送信される
    <form method="post" action="/users/add">
  • エンティティーを引数に取りつつ、アクションを指定
    <使用例>
    //UsersControllerのアクションのビュー内で
    <?= $this->Form->create($user,['action' => 'other']); ?>
    
    <出力結果>
    <form method="post" action="/users/other">

フォームを閉じるためにはend()を使用します。

<使用例>
<?= $this->Form->end(); ?>

<出力結果>
</form>

controlメソッド

フォームの開始タグから終了タグまでの間には、control()メソッドを使って複数のフォーム要素を設置することができます。

$this->Form->control(フィールド名,オプション);

Form->create()メソッドにて、エンティティオブジェクトを引数に取っている場合には、control()メソッドの第一引数にエンティティの持つフィールド名を指定することで、そのフィールドに設定されているデータ型(string,boolernなど)を読み込み、自動的に適切な入力フォームのタイプ(text,checkboxなど)が選択されます。

使用するタイプを手動で指定する必要がある場合にはオプションで指定することも可能です。
また、nullを禁止にしているフィールドの場合には、自動的にrequiredの属性が入ります。

以下、controlメソッドの使用例となります。

  • フィールド名のみ
    <使用例>
    <?= $this->Form->create($users); ?>
    
    // usersテーブルのusernameカラムを指定
    // 自動的に要素タイプはtextとして生成される
    <?= $this->Form->control('username'); ?>
    
    <出力結果>
    <div class="input text required">
      <label for="username">Username</label>
      <input type="text" name="username" required="required" data-validity-message="This field cannot be left empty" id="username" aria-required="true" maxlength="255">
    </div>
    
    
  • オプションを指定
    <使用例>
    <?= $this->Form->control('name', [
        'type' => 'text',
        'label' => 'Your Name',
        'value' => 'cake php',
        'required' => true,
        'disabled' => false,
    ]); ?>
    
    <出力結果>
    <div class="input text required">
      <label for="name">Your Name</label>
      <input type="text" name="name" required="required" id="name" aria-required="true" value="cake php">
    </div>
    
    

    オプションには以下のものがあり、様々なケースに対応することが可能です。

    • type : フィールドのタイプを指定します。
      • text : 一行テキスト入力
      • password : パスワード入力
      • textarea : テキストエリア
      • checkbox : チェックボックス
      • radio : ラジオボタン
      • select : セレクトボックス
      • datetime : 日付と時刻の入力欄
      • date : 日付の入力欄
      • time : 時刻の入力欄
    • label : ラベルとして表示するテキストを指定します。
    • value : 初期値を指定します。
    • options : オプションの配列を指定します。主にセレクトボックスで使用します。
    • empty : セレクトボックスの先頭に表示する空の要素を指定します。
    • disabled : フィールドを無効にする場合に true を指定します。
    • readonly : フィールドを読み取り専用にする場合に true を指定します。
  • セレクトボックスの例
    <使用例>
    //セレクトボックス用に連想配列データの作成
    <?php
    $options = [
        '1' => 'type 1',
        '2' => 'type 2',
        '3' => 'type 3'
    ];
    ?>
    
    //optionsにデータをセット、
    <?= $this->Form->control('type_id', [
        'type' => 'select',
        'options' => $options,
        'empty' => '選択してください',
        'label' => 'type select'
    ]); ?>
    
    
    <出力結果>
    <div class="input select">
      <label for="type-id">type select</label>
      <select name="type_id" id="type-id">
        <option value="">選択してください</option>
        <option value="1">type 1</option>
        <option value="2">type 2</option>
        <option value="3">type 3</option>
      </select>
    </div>
    
    

ボタンの作成

ボタンの作成にはsubmit()とbutton()メソッドを使う方法があります。単純なフォーム送信であれば、submit()で十分ですが、javascriptの処理を発生させるなど、フォーム送信以外に使用するボタンを作る場合などにはbutton()を使用します。

  • summit()の使用例
    <使用例>
    <?= $this->Form->submit('送信'); ?>
    
    <出力結果>
    <div class="submit"><input type="submit" value="送信"></div>
    
    
  • button()の使用例
    <使用例>
    //オプションでbuttonを指定
    <?= $this->Form->button('OK',['type' => 'button']); ?>
    
    <出力結果>
    <button type="button">OK</button>
    
    
    

    フォームの入力をリセットするためのボタンを作成することも出来ます。

    <使用例>
    //オプションでresetを指定
    <?= $this->Form->button('入力フォームのリセット',['type' => 'reset']); ?>
    
    <出力結果>
    <button type="reset">入力フォームのリセット</button>
    
    

その他のヘルパー

numberヘルパー

通貨の表示や、小数点以下の表示設定など様々な数値のフォーマットを整えるために使われます。以下、使用例となります。

  • currency()メソッド

    通貨フォーマットを ユーロ・英ポンド・米ドル から指定して表示

    <使用例>
    <?= $this->Number->currency('1234.56', 'EUR'); ?>
    
    <出力結果>
    €1,234.56

    第二引数で通貨フォーマットをEUR,GBP,USDのいずれかに指定することが可能です。
    またconfig/app.php に記述のある「defaultLocale」の値を'en_US'から'ja_JP'に変更することで、第二引数を指定しないデフォルト表示を、円マークでの表示とすることも出来ます。

  • precision()

    浮動小数点のフォーマットで出力することができ、第二引数で小数点以下を何桁目まで表示するかを指定することが出来ます。

    <使用例>
    <?= $this->Number->precision(345.678921, 2); ?>
    
    <出力結果>
    345.68
  • toPercentage()

    パーセントでの出力を行います。小数点以下はデフォルトで2桁目までの表示ですが、第二引数で指定することも出来ます。

    <使用例>
    <?= $this->Number->toPercentage(34.67865,3); ?>
    
    <出力結果>
    34.679%

    また、入力した値×100をしてパーセント表示する必要がある場合には第三引数のオプションにmultiply' => true を入れることで対応可能です。

    <使用例>
    <?= $this->Number->toPercentage(0.45691, 1, ['multiply' => true]); ?>
    
    <出力結果>
    45.7%
  • toReadableSize()

    入力したデータサイズをKB,GBなどで表示

    <使用例>
    <?= $this->Number->toReadableSize(12345678); ?>
    
    <出力結果>
    11.77 MB
  • format()

    小数点以下の桁数を指定したり、数値の前後にテキストを入れ込むことを可能にします。

    <使用例>
    <?= $this->Number->format('123.7854', [
        'places' => 2, //小数点以下の桁数を指定
        'before' => '約', //数値の前に表示するテキスト
        'after' => 'cm' //数値の後に表示するテキスト
    ]); ?>
    
    <出力結果>
    約123.785cm

textヘルパー

テキスト内のURLにリンクを付けたり、指定した単語の強調やその前後の抜粋、長いテキストの切り詰めなどの便利なメソッドが用意されています。

また、textヘルパーを通してテキストを出力する際には自動的にHTMLエスケープがかかるため、セキュリティ上のリスクを低減することにもつながります。(出力内容にユーザーによる悪意のあるテキストが含まれる可能性のある場合など)

以下、textヘルパーの主張なメソッドを紹介していきます。

  • autoLinkEmails()メソッド

    テキスト内に含まれるメールアドレス検出し、自動でリンクを付与して出力します。

    <使用例>
    <?php
    $text = 'つぎのメールアドレスにお問い合わせ下さい。
    info@example.com';
    ?>
    
    <?= $this->Text->autoLinkEmails($text); ?>
    
    <出力結果>
    つぎのメールアドレスにお問い合わせ下さい。
    <a href="mailto:info@example.com">info@example.com</a>
    
    
    つぎのメールアドレスにお問い合わせ下さい。
    info@example.com
  • autoLinkUrls()メソッド

    テキスト内に含まれるURLを検出し、自動でリンクを付与して出力します。

    <使用例>
    <?php
    $text = 'つぎのURLをご確認下さい。
    info@example.com';
    ?>
    
    <?= $this->Text->autoLinkUrls($text); ?>
    
    <出力結果>
    つぎのURLをご確認下さい。
    <a href="https://cake.com">https://cake.com</a>
    
    
    つぎのURLをご確認下さい。
    https://cake.com
  • autoLink()メソッド

    テキスト内に含まれるURLとメールアドレス両方のを検出し、自動でリンクを付与して出力します。

    <使用例>
    <?php
    $text = 'URLから内容をご確認のうえ、メールアドレスまでご連絡ください
    URL : https://cake.com / mail : info@example.com';
    ?>
    
    <?= $this->Text->autoLink($text); ?>
    
    
    <出力結果>
    つぎのURLをご確認のうえ、記載のメールアドレスまでご連絡ください
    URL : <a href="https://cake.com">https://cake.com</a>
    / mail : <a href="mailto:info@example.com">info@example.com</a>
    
    
    つぎのURLをご確認のうえ、記載のメールアドレスまでご連絡ください
    URL : https://cake.com/ mail : info@example.com
  • autoParagraph()

    テキスト内に1度の改行がある場合はbrタグを生成し、2回以上の改行の場合は新規のpタグを生成することで、自動の段落分けを行います。

    <使用例>
    <?php
    $text = 'Lesson1では導入として
    PHPやフレームワークの概要についても触れていきます。
    
    それでは早速学んでいきましょう。';
    ?>
    <?= $this->Text->autoParagraph($text); ?>
    
    
    <出力結果>
    <p>Lesson1では導入として<br>
    PHPやフレームワークの概要についても触れていきます。</p>
    <p>それでは早速学んでいきましょう。</p>
    
    
    

    Lesson1では導入として
    PHPやフレームワークの概要についても触れていきます。

    それでは早速学んでいきましょう。

  • highlight()メソッド

    テキスト内の特定の文字列に指定のHTMLを付与して出力することが出来ます。

    以下の例では、highlight()メソッドの第一引数にテキスト、第二引数にHTMLを付与したいキーワード、第三引数に付与する内容としてstyleを指定しています。

    <使用例>
    <?php
    $keyword = "WEB";
    $text = "PHPを使ってWEBサイトを制作します。";
    ?>
    
    <?= $this->Text->highlight($text, $keyword, ['format' => '<span style="background-color:#ffff80;">\1</span>']); ?>
    
    
    <出力結果>
    PHPを使って
    <span style="background-color:#ffff80;">web</span>
    サイトを制作します。
    
    
    PHPを使ってwebサイトを制作します。

    今回の例では第三引数にstyleを直書きしていますが、classを付与するようにして、用意しておいたハイライト用のcssを適用させることなども可能です。

  • truncate()、 tail()、 excerpt()メソッド

    長文のテキストの冒頭のみを表示させる場合や、検索機能などで特定のキーワードの前後のテキストのみを表示させたい場合などに有用なメソッドとなります。

    省略方法にはtruncate()→「先頭のみ...」、tail()→「...末尾のみ」、excerpt()→「...キーワードの前後のみ...」の3つがあります。

    <使用例>
    <?php
    $text = 'Lesson1では導入としてPHPやフレームワークの概要についても触れていきます。それでは早速学んでいきましょう。';
    $length = 12; //表示する文字数
    ?> 
    
    <?= $this->Text->truncate($text,$length); ?>
    <出力結果> Lesson1では...
    
    
    <?= $this->Text->tail($text,$length); ?>
    <出力結果> ...んでいきましょう
    
    
    <?= $this->Text->excerpt($text,'フレームワーク',$length); ?>
    <出力結果> ...1では導入としてPHPやフレームワークの概要についても触れてい...

独自のヘルパーを作成する

ここまでCakePHPで用意されているヘルパーを見てきましたが、1から自作のヘルパーを作成したり、既存のヘルパーをカスタマイズする形で独自のヘルパーを作成することも可能です。

ヘルパーを作成する際には規約がいくつかあるため、以下にまとめます。

  • 作成したヘルパークラスのファイルはsrc/View/Helperの配下に置く
  • ファイル名とクラス名は一致させる、
  • 命名は末尾に「Helper」をつける必要がある
  • 作成したヘルパークラスにはHelperクラスを継承させる
  • ヘルパーを参照する際には語尾の「Helper」を省略する必要がある

例として、Htmlヘルパーのlinkメソッドで生成したリンクにclassを付与して出力を行う MyHelper を作成してみましょう。以下の流れに沿って作業を進めて下さい。

  1. src/View/Helperの配下にMyHelper.phpを作成
  2. Helperクラスを継承させて自作のヘルパークラスを作成

    MyHelper.php
    <?php
    
    namespace App\View\Helper;
    
    use Cake\View\Helper;
    
    class MyHelper extends Helper
    {
        public $helpers = ['Html'];
    
        public function makeMyUrl($title, $url)
        {
            // 出力に HTML ヘルパーを使用
            // 整形されたデータ:
    
            $link = $this->Html->link($title, $url);
    
            return '<div class="myUrl">' . $link . '</div>';
        }
    }
    

    既存のヘルパーを自作ヘルパー内で使いたい場合には、$helpersプロパティに配列として使用するヘルパーを記述して代入することで使用可能となります。今回はHtmlヘルパーを指定しています。

    処理内容としては、linkメソッドで返ってきた値にdivタグを加え、classを付与した後に返しています。

  3. 使用するビュー内でヘルパーを呼び出します。今回はtemplates/Tasks/index.php にて使用し、ユーザー一覧ページへのリンクとして設置してみます。

    index.php
    <h1>タスク一覧</h1>
    
    <!-- 自作ヘルパー -->
    <?= $this->My->makeMyUrl('Usersページ','users/'); ?>
    
    <?= $this->html->link('新規作成',['action'=>'add'])?>
    <table>

    ヘルパークラスを呼び出す際は語尾のHelperを省略するため、「My」のみとなっています。

  4. タスク一覧ページを開くと「Usersページ」というリンクが作成されており、ブラウザの検証機能などでコードを確認すると、以下のようにdivタグによりclassが付与されており、正常に自作ヘルパーによる処理が行われていることが確認出来るかと思います。

    <div class="myUrl">
    <a href="/users/">Usersページ</a>
    </div>
    
    

以上で自作ヘルパーの作成例は終了です。ビューの中で何度も使う機能が出てきたり、既存のヘルパーの処理を少し変更したい場合などには自作ヘルパーを作成し開発効率の改善を試みてみましょう。

Lesson 10 Chapter 2
コンポーネント

コンポーネントは、主にコントローラーで共通して複数箇所で使われるような機能を簡単に再利用できるようにするための仕組みです。コンポーネントを使用することで、同じような処理を複数のコントローラーで何度も実装する必要がなくなり、コードの再利用性が向上するというメリットがあります。

コンポーネントを使用するには、src/controller/AppController.php もしくは、使用するコントローラークラス内のinitializeメソッドでloadComponent()メソッドを使って明示しておく必要があります。

以下、主要なコンポーネントの説明をしていきます。

Authenticationコンポーネント

Authenticationコンポーネントは Lesson8 の認証機能の実装でも使用しており、src/controller/AppController.php に以下のように設定をして呼び出していました。

AppController.php
//initializeメソッド内にて以下の行を追加
$this->loadComponent('Authentication.Authentication');

上記のようにComponentを使用するにはコントローラクラスの持っているloadComponent()メソッドを使用し、引数にコンポーネントの名称をとるのですが、Authenticationコンポーネントについては、Authenticationプラグインの中にパッケージされているため、「プラグイン名.コンポーネント名」という形で引数を記述しています。

読み込んだコンポーネントはコントローラ内で以下のようにコンポーネントクラスのインスタンスを呼び出す形で使用することができます。

$result = $this->Authentication->getResult();

上記ではAuthenticationクラスのインスタンスを呼び出し、getResult()メソッドを利用しています。

Authenticationコンポーネントには認証機能に必要な機能やメソッドがまとめられており、認証結果の確認や、認証を必要としないアクションの指定、ログアウトの実行などを行うことが出来ます。

ここで改めて、 Lesson8 で実装したログインアクションを見てみましょう。

UsersController.php
public function login()
{
    // POST,GETを問わずユーザーの認証結果を取得
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();
    //認証結果がある かつ 認証結果がtrueならば
    if ($result && $result->isValid()) { //
        //tasks/indexへとリダイレクト
        $redirect = $this->request->getQuery('redirect', [
            'controller' => 'Tasks',
            'action' => 'index',
        ]);
    
        return $this->redirect($redirect);
    }
    // ユーザーがログインに失敗した場合は、エラーを表示します
    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error(__('username か passwordに誤りがあります'));
    }
}

postかgetでのリクエストであることを確認した後に、AuthenticationコンポーネントからgetResult()メソッドを使用し、その結果を$resultへと代入しています。

このとき、$resultには以下のようにAuthentication\Authenticator\Result のオブジェクトとしての値が代入されています。

//認証がされていない場合のresult
Authentication\Authenticator\Result Object
(
    [_status:protected] => FAILURE_CREDENTIALS_MISSING
    [_data:protected] => 
    [_errors:protected] => Array
        (
            [0] => Login credentials not found
        )

)
認証済みの場合のresult
Authentication\Authenticator\Result Object
(
    [_status:protected] => SUCCESS
    [_data:protected] => App\Model\Entity\User Object
        (
            [id] => 1
            [username] => cake
            [password] => $2y$10$yITFmlLMjJemY5U/ElSFhOBBBv606P3KsskeEtQ2V152ip2HeWohy
            [created_on] => 
            [modified_on] => Cake\I18n\FrozenTime Object
                (
                    [date] => 2023-05-06 21:35:59.000000
                    [timezone_type] => 3
                    [timezone] => UTC
                )

            [_locale] => ja
            [[new]] => 
            [[accessible]] => Array
                (
                    [username] => 1
                    [password] => 1
                    [created] => 1
                    [modified] => 1
                )
                ...

_statusには認証の結果が示され、_dataには認証されたユーザーのデータが入っていることが分かります。このResultオブジェクトには上記のデータの他に、以下のメソッドも実装されており、Resultオブジェクト内のデータを処理内容にあわせて取得することが出来ます。

  • isValid() メソッド

    認証が成功したかどうかを示すブール値を返します。認証が成功した場合はtrue、失敗した場合はfalseです。ログインアクション内でも使われていました。

  • getStatus() メソッド

    Resultオブジェクトの_statusの値を取得します。認証が成功した場合はSUCCESS、それ以外はFAILURE...となります。

  • getData() メソッド

    Resultオブジェクトの_dataの値を取得します。基本的には認証に成功したユーザーの情報を取得する形となります。

これらのメソッドを前提に、認証結果による場合分けや、認証に成功したユーザーのデータを使用してより複雑なログインメソッドの処理を検討することが可能です。

次に、認証を入れないアクションを指定する機能について見ていきます。 Lesson8 では以下のように指定していました。

UsersController.php
public function beforeFilter(\Cake\Event\EventInterface $event)
{
    parent::beforeFilter($event);
    $this->Authentication->addUnauthenticatedActions(['login']);
}  

Authenticationコンポーネントの持つ addUnauthenticatedActions()メソッドを使用して、ログインアクションでは認証を行わないようにしています。

仕組みとしては、Authenticationコンポーネントがそのクラス内で、$unauthenticatedActionsというプロパティを宣言しており、このプロパティに配列データとして代入されたアクションは認証が行われないようになっています。
そのため、$addUnauthenticatedActions()メソッドを使用して$unauthenticatedActionsプロパティに値を追加していたという訳です。

最後にlogout()メソッドについてです。こちらは Lesson8 で実装したlogoutアクション内で以下のように使用されていました。

public function logout()
{
    $result = $this->Authentication->getResult();
    if ($result && $result->isValid()) {
        $this->Authentication->logout();
        $this->Flash->success(__('ログアウトしました'));
        return $this->redirect(['controller' => 'Users',
'action' => 'login']);
        
    }
}

$this->Authentication->logout();となっており、Authenticationコンポーネントからlogout()メソッドを呼び出し実行しています。

logout()メソッドは内部的にリクエストとレスポンスのデータを取得し、認証情報のみを削除して、再度リクエストとレスポンスにデータをセットし直すことでログアウトを実現しています。

以上、Authenticationコンポーネントの主要な機能やメソッドについて見てきましたが、いずれも複雑になりがちな処理を、Authenticationコンポーネントの持つメソッドなどを使用することで、無駄な記述をすることなくログイン/ログアウトなどの機能を実装することが出来るようになります。

Security コンポーネント

SecurityComponent は、悪意あるユーザーによるフォームの改ざんを防ぐことを目的としており、セキュリティ面のリスクを低減させるために使用します。

具体的には以下の動作の禁止を行います。

  • フォームに新規フィールドを追加すること
  • フォームからフィールドを削除すること
  • hidden フィールドの値を更新すること

使用するにあたって難しいことはなく、適用させるコントローラのクラスにinitialize()メソッドを設置し、loadComponent()メソッドの引数としてSecurityComponentの使用を明示をすることで自動的に適用されます。

//任意のコントローラークラス内にて

public function initialize(): void
{
    parent::initialize();
    $this->loadComponent('Security');
}

ただしSecurityComponentを使う場合には、必ずFormHelperを使用してフォームの作成を行う必要があります。

これはフォームの改ざんがあったかを判断するために、FormHelperを使って作られたフォーム内のフィールドを全てまとめたデータをハッシュ化しておき、その後 POSTデータから再現したデータと突き合わせるという方法をとっているためとなります。

注意してほしいこと

セレクトボックスやラジオボタンにおけるオプションの追加と変更を防ぐことは出来ません。

Paginator コンポーネント

Paginatorコンポーネントはタスク一覧やユーザー一覧ページのように、複数のデータを出力する際に、データを分割して複数のページに分けて表示出来るようにする機能を提供するコンポーネントとなります。

データの総数が大きくなればなるほど、一度に全てのデータを読み込み、出力する際の負荷は大きくなりますし、表示されるデータが多すぎるとユーザーからしてもページが見ずらく、使い勝手が悪くなる可能性があります。

Paginatorコンポーネントでは、こういった問題を解決するために、1ページに表示するデータの数を制限したり、データの並び順を操作したりする機能が備わっており、アプリケーションのパフォーマンスとユーザビリティの向上に役立てることが出来ます。

ただ、ページネーション機能の実装にはPaginatorコンポーネントとPaginatorヘルパーの両方を使用する方法が一般的であるため、ここではPaginatorコンポーネントの概要の説明までとし、Paginatorヘルパーを含めたページネーション機能の実装については Lesson11 にてあらためて説明を行います。

Paginatorコンポーネントも、ほかのコンポーネント同様に、使用するコントローラクラスのinitialize()メソッド内にてloadComponent()メソッドを使い、'Paginator'を読み込むことで使用可能となります。

次に使い方ですが、例として Lesson7 で作成したUsersContorollerクラスのindexアクションを挙げます。

public function index()
    {
        $users = $this->paginate($this->Users);

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

気を付ける点としてpaginate()メソッドがコンポーネントからではなく、コントローラから直接呼び出されていることが挙げられます。これはPaginatorコンポーネントの持つ例外であり、基本的には$this->Component->method() のように、コンポーネントからでなければメソッドを呼び出すことはできません。

Paginatorコンポーネントにこのような例外を持たせている理由としては、paginate()メソッドはコントローラに$paginateというプロパティがあった場合、それをオプションの値として参照して実行される点にあります。

例えば、以下のように$paginateプロパティを同コントローラーのクラス内に設置することで、paginate()メソッドの働きを操作することができます。

public $paginate = [
  'limit' => 10,
  'order' => [
    'users.created' => 'asc'
  ]
];

内容としては、レコードの表示数を10までとし、作成日の昇順で並べるというものです。

このようにコントローラー内に$paginateを置いて使用する場合、仮にコンポーネントからメソッドを呼び出してしまうと$paginateが参照されなくなります。
そのため、$this->paginate();のようにコントローラから直接呼び出す形をとっています。

オプション用に$paginateを置くメリットですが、クラス内で何度もpaginate()メソッドを使用する場合に、その都度オプションの設定をする必要がなくなります。

また、もしコンポーネントから呼びだす形にする必要がある場合は、以下のように第二引数にオプションを指定することが出来ます。

$users = $this->Paginator->paginate($query, ['limit' => 10]);

paginate()メソッドにて分割されたデータは、set()メソッドでビュー側に送ることで、ページ単位で分けて表示をすることが出来るようになります。

独自のコンポーネントを作成する

コンポーネントを作成する場合にはヘルパーと同様に、以下のような規約があります。

  • 作成したコンポーネントはsrc/Controller/Component配下に保存する。
  • コンポーネントのファイル名とクラス名は一致させる。
  • 命名は末尾に「Component」をつける必要がある
  • Componentクラスを継承させる。
  • 呼び出す場合には語尾のComponentは省略する

例としてコンポーネントの作成をしていきます。今回はCalcComponentという命名で、bakeコマンドを使用してひな形を作成します。

bin/cake bake component Calc

上記のコマンドを実行すると、src/Controller/Component/CalcComponent.phpファイルが生成され、中身を確認するとひな形が出来上がっている状態となります。

次に、CalcComponentクラスにメソッドを追加します。

public function doAddn($num1, $num2){

    return $num1 + $num2;
}

これでコンポーネントの作成は完了となるので、ほかのコンポーネントと同様に、コントローラのinitializeメソッドで読み込ませます。

   public function initialize(): void
    {
        parent::initialize();
        $this->loadComponent('Calc');
    }

コントローラのメソッド内で以下のように呼び出して使うことが出来るようになります。

$result = $this->Calc->doAddn(1,2);

Lesson 10 Chapter 3
ビヘイビア

ビヘイビア(Behavior)は、モデルの機能を拡張するための再利用可能なコードです。ビヘイビアを使うことで、同じ機能を複数のモデルで共有することができます。

ビヘイビアの大きな特徴として通常のメソッドとは別に、特定のイベントが起こった際に発動するイベントリスナーとしてのメソッドを使える点が挙げられます。これは、エンティティにデータが入る時や、データベースに値が保存された直後など特定のタイミングにおいて発動させることが可能となります。

仮にこれを通常のメソッドを使用して実現しようとすると、たとえばデータベースに値が保存された直後でメソッドを実行させる場合、save()メソッド内で行われている処理を正確に理解し、正しい位置にメソッドを配置する必要が出てきてしまい、複雑な作業を強いられることとなります。

そのため、イベントリスナーとしてのメソッドを使える点はビヘイビアの大きなメリットとなります。

ビヘイビアの使い方

使い方はコンポーネントなどと同様に、モデルのテーブルクラス内にinitialize()メソッドを置き、その中でaddBehavior()メソッドを用いて使用するビヘイビアを読み込ませる形となります。

また、読み込まれたビヘイビアが持つメソッドは自動的にテーブルクラスへと追加されるため、呼び出す際にはビヘイビアを通すのではなく、テーブルクラスから直接呼び出して使用します。

早速、代表的なCakePHPのビヘイビアを見ていきますが、次に紹介するTimeStampビヘイビアはイベントリスナーとしてのビヘイビアとなっています。

Timestamp ビヘイビア

Timestamp ビヘイビアは、イベント発生のたびにテーブルの持つ created や modified フィールドにタイムスタンプとして現在時刻の値を自動的に入力し更新するのに使用されます。

Timestampビヘイビアを適用するには、使用するテーブルクラスのinitialize()メソッド内でaddBehavior()メソッドを使って読み込ませます。

class UsersTable extends Table
  {
      public function initialize(array $config): void
      {
          parent::initialize($config);
          $this->addBehavior('Timestamp');
          ...

デフォルトでは新規のエンティティを保存するときにcreated と modified に現在の日時が入り、既存のEntity を更新したときには、modifiedのみに現在の日時が保存されます。

これらの設定を変更したい場合には、addBehavior()メソッドでTimestampビヘイビアを読み込ませる際に変更内容を指定することができます。例えば、タイムスタンプを行うフィールド名がcreatedとmodifiedではなく、created_onとmodified_onであった場合には以下のように設定します。

$this->addBehavior('Timestamp', [
  'created' => 'created_on',
  'modified' => 'modified_on',
  'events' => [
      'Model.beforeSave' => [
          'created_on' => 'new', 
          'modified_on' => 'always', 
      ]
  ]
]);

addBehavior()メソッドの第二引数で連想配列を取り、最初の「'created' => 'created_on'」で保存するフィールド名の変更を指定しています。その後のevents では「Model.beforeSave」でインスタンスの保存前に起こるイベントを指定しており、それぞれのフィールドにタイムスタンプを行う条件としてnew = 新規保存のみ、always = 新規・編集の両方 という設定をしています。

このように、timestampビヘイビアは特定のイベントが発生した際に自動的に処理を行うイベントリスナーとしてのビヘイビアとなります。

Tree ビヘイビア

Treeビヘイビアは1つのテーブル内において親子関係を持ち、階層構造を作っているレコードを扱います。 Lesson6 で紹介をした find('threaded') でも parent_id カラムを持つテーブルのデータを親と子からなる階層構造として扱い、取得することが出来ましたが、Treeビヘイビアではparent_idカラムに加えて「lft」,「rght」というカラムを追加し、同じ親を持つ子レコードの順番を操作するなど、より複雑な操作を可能とします。

そのため、メニューの表示順序を変更したい場合や、ユーザーが任意の順番でデータを並び変えたい場合の処理などで有用です。

Treeビヘイビアの基本的な機能を確認するために、既存のtasksテーブルにtreeビヘイビアを適用し、実際にその働きを見ていくことにしましょう。

まずはほかのビヘイビア同様にaddBehavior()メソッドを使いtreeビヘイビアを読み込みます。今回はsrc/Model/Table/TasksTable.php となります。

TasksTable.php
class TasksTable extends Table
{
    public function initialize(array $config): void
    {
        $this->addBehavior('Tree');
    }
}

つぎにtreeビヘイビアを使用するために必要な以下3つのカラムを追加していきます。

  1. parent_id
  2. lft
  3. rght

parent_idは親レコードの主キーを保持し、lftとrghtは階層構造を表すための数値を入れるために使われます。lftとrghtはレコードが追加される度に自動更新されるため、手動で値を操作することはありません。

MySQLのコマンドライン上で以下を入力し、カラムの追加を行います。

ALTER TABLE tasks
ADD COLUMN parent_id INT,
ADD COLUMN lft INT,
ADD COLUMN rght INT;

カラムを追加後、テーブルを確認するとparent_id,lft,rghtいずれもnullの状態になっています。

10_3_1.png

lft,rghtはモデルを通して自動更新されるのですが、すでにレコードが存在するテーブルにカラムを追加した場合などには、今回のようにnullとなってしまいます。

これを解消するために、treeビヘイビアのrecover()メソッドを使用して、テーブルの階層構造を再構築させます。

今回は簡単に実行を行うために、TasksControllerのindexアクションにrecover()メソッドを仕込みます。

TasksController.php
public function index()
{
  $this->Tasks->recover();//テーブルの階層構造を再構築

  $tasks = $this->Tasks->find('all');
  $this->set(['tasks' => $tasks]);
}

ビヘイビアの持つメソッドは自動でテーブルクラスに読み込まれるため、Tasksから直接recover()メソッドを呼び出しています。

記述が完了したら、ブラウザでタスク一覧画面を開き、recover()メソッドを実行させましょう。その後に再度MySQLのコマンドラインでテーブルの内容を確認すると、以下のようにlftとrghtの値が更新されているはずです。

10_3_2.png

この状態ではまだ親となるレコードしかないため、次に子となるレコードを作成し、階層構造を作ります。以下のインサート文を再度MySQLのコマンドラインで入力します。

INSERT INTO tasks (content, parent_id)
VALUES ('PHPのインストール', 1),
       ('Composerのインストール', 1),
       ('MySQLのインストール', 2),
       ('データベース作成', 2);

その後、再度タスク一覧画面を開き直しrecover()メソッドを実行させたのちに、コマンドライン上でtasksテーブルの内容を確認すると以下のようになっています。(recover()メソッドは削除かコメントアウトしてしまって大丈夫です。)

10_3_3.png

parent_idには親となるレコードのidの値を持たせており、更新されたlftとrghtを見ると、親レコードの持つ数の間の数字で子レコードのlftとrghtの値が構成されています。

例えば、idが1の親レコードはlftに「1」、rghtに「6」を持っており、その子レコードであるid3と4のlftとrghtを見ると1と6の間に含まれる「2~5」で構成されていることがわかるかと思います。

このように、親レコードの持つlft、rghtの数の間に含まれる数字を使って子レコードにlft、rghtの値を持たせることで階層構造を表していきます。

次にTreeビヘイビアのメソッドを使って子レコードのデータを取り出し、タスク詳細ページに表示をしてみます。src/Contorller/TasksController.phpを開き、detail()アクションを以下のように編集しましょう。

TasksController.php
public function detail()
{
  $id = $this->request->getQuery('id');
  $task = $this->Tasks->get($id);

  $children = $this->Tasks->find('children', ['for' => $id]);
  
  $this->set([
    'task' => $task,
    'children' => $children,
  ]);
}

選択した親レコードの子レコードを取得するために、find('children', ['for' => $id])を使用しています。'children'と'for'は固定であり、$id部分に親レコードの主キーを入れることで、その親の子レコードデータをすべて検索することができます。

また、taskとchildren 2つの変数をビューテンプレートで使用できるように、set()メソッドは配列形式としています。

子レコードのデータが準備できたので、ビューテンプレートである templates/Tasks/detail.phpを編集し、子レコードの値を表示してみましょう。以下をdetail.phpのtableタグのあとに追加します。

<h2>子タスク</h2>
<ul>
    <?php foreach ($children as $child): ?>
        <li>
            <?= h($child['content']) ?>
            <?php if ($child['children']): ?>
                <ul>
                    <?php foreach ($child['children'] as $grandchild): ?>
                        <li>
                            <?= h($grandchild['content']) ?>
                        </li>
                    <?php endforeach; ?>
                </ul>
            <?php endif; ?>
        </li>
    <?php endforeach; ?>
</ul>

記述ができたら、親レコードの詳細画面を開いてみましょう。下部に子タスクの表示がされているはずです。

10_3_4.png

次に子タスクの順番を替えてみます。順番の変更にはTreeビヘイビアのmoveUp()やmoveDown()メソッドを使用します。今回はタスク詳細画面からリンクを使って、moveUp()とmoveDown()を呼び出せるようにするため、以下2つのメソッドをTasksController.phpに追加します。

TasksController.php
  public function moveUp()
{
  $id = $this->request->getQuery('id');
  $entity = $this->Tasks->find()->select(['id','parent_id','lft','rght'])->where(['tasks.id' => $id])->first();
  $this->Tasks->moveUp($entity);
  if($this->Tasks->save($entity)) {
    return $this->redirect(['action' => 'detail','?' => ['id' => $upEntity->parent_id]]);
  };
}

public function moveDown()
{
  $id = $this->request->getQuery('id');
  $entity = $this->Tasks->find()->select(['id','parent_id','lft','rght'])->where(['tasks.id' => $id])->first();
  $this->Tasks->moveDown($entity);
  if($this->Tasks->save($entity)) {
    return $this->redirect(['action' => 'detail','?' => ['id' => $upEntity->parent_id]]);
  };
}

どちらもgetQuery()メソッドで順番を替えたい子レコードのidを取得したのち、find()のチェーンメソッドでparent_idなど、TreeビヘイビアのmoveUp()とmoveDown()メソッドに必要なカラムを含めたエンティティオブジェクトを取得しentityに代入しています。

その後にmoveUp(),moveDownを実行し、lftとrghtカラムの値が変更されるため、その内容をsave()メソッドで保存しています。

次に、ビューテンプレートを編集し、作成したmoveUp()、moveDown()メソッドを呼び出すためのリンクを設置します。

さきほど、記述した子タスクの内容部分を以下に書き換えてください。

<h2>子タスク</h2>
<ul>
    <?php foreach ($children as $child): ?>
        <li>
            <?= h("id :".$child['id']." ".$child['content'])." ".$this->Html->link('move_up' , "tasks/moveUp?id=".h($child['id']))." / ".$this->Html->link('move_down' , "tasks/moveDown?id=".h($child['id']))?>
            <?php if ($child['children']): ?>
                <ul>
                    <?php foreach ($child['children'] as $grandchild): ?>
                        <li>
                        <?= h("id :".$grandchild['id']." ".$grandchild['content'])." ".$this->Html->link('move_up' , "tasks/moveUp?id=".h($grandchild['id']))." / ".$this->Html->link('move_down' , "tasks/moveDown?id=".h($grandchild['id']))?>
                            <!-- 追加のネストされたレベルがあれば、同様に続けて処理 -->
                        </li>
                    <?php endforeach; ?>
                </ul>
            <?php endif; ?>
        </li>
    <?php endforeach; ?>
</ul>

記述を終えたら一度、タスク詳細画面を開いてみましょう。idの値と、moveUp()、moveDown()メソッドへのリンクが追加されているのがわかるかと思います。

10_3_5.png

確認ができたら、早速いずれかのリンクを押下して動作を確認してみましょう。今回は子タスクが2つしかないため、上段のものをmoveUp()したり、下段のものをmoveDown()させても特に何も起こりません。上下のデータが入れ替わるように操作ができれば成功です。

最後に、順番を変更する前と後でのlftとrghtの値の変化を確認します。以下が変更前と後でテーブルのデータをlftカラムの昇順で取得したものとなります。

mysql> select id,content,parent_id,lft,rght from tasks where id in (1,3,4) order by lft asc;

+----+-------------------------+-----------+------+------+
| id | content                 | parent_id | lft  | rght |
+----+-------------------------+-----------+------+------+
|  1 | CakePHPの環境構築を行う |      NULL |    1 |    6 |
|  3 | PHPのインストール       |         1 |    2 |    3 |
|  4 | Composerのインストール  |         1 |    4 |    5 |
+----+-------------------------+-----------+------+------+
3 rows in set (0.01 sec)

mysql> select id,content,parent_id,lft,rght from tasks where id in (1,3,4) order by lft asc;

+----+-------------------------+-----------+------+------+
| id | content                 | parent_id | lft  | rght |
+----+-------------------------+-----------+------+------+
|  1 | CakePHPの環境構築を行う |      NULL |    1 |    6 |
|  4 | Composerのインストール  |         1 |    2 |    3 |
|  3 | PHPのインストール       |         1 |    4 |    5 |
+----+-------------------------+-----------+------+------+
3 rows in set (0.01 sec)

idの値は変わることなく、lftとrghtの値のみが変わり、表示順序を変更していることがわかるかと思います。

このように、Treeビヘイビアを使うことでテーブルの物理的な順序はそのままに、表示の順序のみを替えることが可能となります。

その他のビヘイビア

CounterCache ビヘイビア

CounterCacheビヘイビアは、例えば投稿された記事にコメントがついている場合に、そのコメントの数を数える。または、SNSなどにおいてアカウントが持っているフォロワーの数を数えるなどといった用途で使われます。

CounterCacheビヘイビアはアソシエーションが1対多の関係にある2つのテーブル間で使用されることを前提としています。
また、1対多の1に当たるテーブルにはカウントされた数を保存するカラムを用意しておく必要があります。

既存のusersテーブルと、tasksテーブルを使ってその機能を具体的に見ていきましょう。

まず最初にusersテーブルにtask_countカラムを追加します。以下のSQL文をMySQLのコマンドライン上で実行しましょう。

ALTER TABLE users ADD COLUMN task_count INT

次に、src/Model/Table/TasksTable.phpに以下のようにCounterCacheBehaviorを追加します。addBehavior()の第二引数ではカウントを表示するテーブルとそのカラム名を指定します。

TasksTable.php
class TasksTable extends Table
{
    public function initialize(array $config): void
    {
        ...

        $this->addBehavior('CounterCache', [
            'Users' => ['task_count']
        ]);
    }
}

また、src/Model/Table/UsersTable.phpには以下のようにアソシエーションの設定がされていることを確認しておきましょう。

class UsersTable extends Table
{
    public function initialize(array $config): void
{
     ...

    $this->hasMany('Tasks');
}

これで設定は完了したので、値を更新するためにタスク一覧画面から新規作成、編集、削除などいずれかの操作を行いましょう。CounterCacheビヘイビアを適用したテーブルのエンティティの更新が行われることで、task_countカラムの更新が行われます。以下のようにテーブルのデータを確認すると、追加したtask_countカラムの値が更新されていることがわかります。

mysql> select id,username,task_count from users where id = 1;
+----+----------+------------+
| id | username | task_count |
+----+----------+------------+
|  1 | cake     |          7 |
+----+----------+------------+

最後にtemplates/Users/index.phpを編集し、ユーザー一覧ページで各ユーザーがいくつのタスクを持っているか分かるようにしてみましょう。

 <thead>
    <tr>
        <th><?= $this->Paginator->sort('id') ?></th>
        <th><?= $this->Paginator->sort('username') ?></th>
        <th><?= $this->Paginator->sort('task_count') ?></th>
        <th><?= $this->Paginator->sort('created') ?></th>
        <th><?= $this->Paginator->sort('modified') ?></th>
        <th class="actions"><?= __('Actions') ?></th>
    </tr>
</thead>
<tbody>
    <?php foreach ($users as $user): ?>
    <tr>
        <td><?= $this->Number->format($user->id) ?></td>
        <td><?= h($user->username) ?></td>
        <td><?= h($user->task_count) ?></td>
        <td><?= h($user->created) ?></td>
        <td><?= h($user->modified) ?></td>

theadとtbody、それぞれにtask_countを追加しました。ユーザー一覧ページを開き、task_countが表示されていれば成功です。

このように、1対多のアソシエーションを持つ関係であればCounterCacheビヘイビアを使って簡単に1対多の「多」の数を扱うことが可能となります。

Translate ビヘイビア

アプリケーションを複数の言語に対応させる際に、データベース上のテーブルで管理・保存しているテキストを翻訳したい場合があります。

このとき、翻訳をかけたいテキストを持つテーブルとは別で、複数言語の翻訳結果を保存することができるテーブルを作成し、Translate ビヘイビアによって紐づけを行うことで自動的に指定の言語でテキストを表示することが可能になります。

翻訳結果を保存するテーブルには、「i18n」という名前を使用します。これは Internationalization (国際化対応) という単語が I の文字に続いて 18 文字あり、N で終わっていることに由来しており、CakePHPがデフォルトで翻訳用のテーブルだと認識をする命名となります。

それでは早速 Translateビヘイビアとi18nテーブルを使って、usersテーブルの一部レコード内容を翻訳して表示できるようにしてみましょう。

まずは、以下のSQL文でi18nテーブルを作成します。

CREATE TABLE i18n (
    id int NOT NULL auto_increment,
    locale varchar(6) NOT NULL,
    model varchar(255) NOT NULL,
    foreign_key int(10) NOT NULL,
    field varchar(255) NOT NULL,
    content text,
    PRIMARY KEY (id),
    UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
    INDEX I18N_FIELD(model, foreign_key, field)
);

続けて、src/Model/Table/TasksTable.phpでTranslateビヘイビアを追加します。

class TasksTable extends Table
{
  public function initialize(array $config): void
  {
    ...

    $this->addBehavior('Translate', ['fields' => ['content']]);
    
  }

注意として、addBehaviorの第二引数では、キー「fields」の値として対象テーブルの翻訳を必要とするカラムを指定しておく必要があります。今回はcontentカラムのみを指定しています。

つぎに、i18nテーブルに翻訳済みテキストを含むレコードを投入しましょう。

mysql> select id,content from tasks where id = 3;
+----+-------------------+
| id | content           |
+----+-------------------+
|  3 | PHPのインストール |
+----+-------------------+

例として、上記のレコードの翻訳をi18nテーブルに投入します。

INSERT INTO i18n (locale, model, foreign_key, field, content)
VALUES ('en', 'Tasks', 3, 'content','Install PHP')
,('zh', 'Tasks', 3, 'content','安装PHP');

結果としてi18nテーブルのレコードは以下のようになります。

mysql> select * from i18n;
+----+--------+-------+-------------+---------+-------------+
| id | locale | model | foreign_key | field   | content     |
+----+--------+-------+-------------+---------+-------------+
|  1 | en     | Tasks |           3 | content | Install PHP |
|  2 | zh     | Tasks |           3 | content | 安装PHP     |
+----+--------+-------+-------------+---------+-------------+

最後に、src/Application.phpを開き、middleware()メソッド内に翻訳を許容する言語を指定します。

//i18nのミドルウェアをuse文で追加
use Cake\I18n\Middleware\LocaleSelectorMiddleware;

...

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
       
        ...

        // 自動翻訳のために追加
        ->add(new LocaleSelectorMiddleware(['en','ja','zh']))

これで翻訳の準備はできたので、まずタスク一覧ページを開きます。この時点では日本語のままです。次に、ブラウザの言語設定を英語に替えてみます

chromeであればブラウザ右上の設定アイコン->設定->言語 の順で言語設定画面を開き、優先言語の一番上に英語を設定します。

10_3_6.png

設定後に再びタスク一覧画面をリロードすると、対象のレコードがi18nテーブルに投入した英語用(en)のテキストに翻訳されているかと思います。

10_3_7.png

その後、同じように中国語を優先言語の一番上に設定したあとにページをリロードし翻訳がされるか確認してみましょう。

10_3_8.png

以上 Translateビヘイビアを使い、テーブルで管理しているテキストを使用言語に併せて自動翻訳する機能を見てきました。

ビューテンプレートに直接記入している文言を、__()とpot/poファイルなどを使用して行う翻訳方法については で説明をします。

独自のビヘイビアを作成する

ビヘイビアもまた、以下の規約に従って自作のものを作ることができます。

  • ビヘイビアファイルはsrc/Model/Behavior/配下に置く
  • ビヘイビアークラスの名前空間は App\Model\Behavior 内にする
  • ビヘイビアークラスの名前の語尾はBehavior とする
  • ビヘイビアーは Cake\ORM\Behavior を継承させる。

今回はsrc/Model/Behavior/配下にMyCustomBehavior.phpを作成し、Treeビヘイビアの説明でTasksControllerクラスに作成したmoveUp()とmoveDown()メソッドの一部をビヘイビアとして機能させてみます。

MyCustomBehavior.phpの作成は手動でもよいですが、コマンドラインでbakeを使用して作成することもできます。その場合はtodo4ディレクトリ配下に移動し、以下の形でコマンドを入力します。

bin/cake bake behavior myCustom

これで簡単にビヘイビアクラスのひな型が作成できました。以下、MyCustomBehavior.phpの内容となります。

<?php
declare(strict_types=1);

namespace App\Model\Behavior;

use Cake\ORM\Behavior;
use Cake\ORM\Table;

class MyCustomBehavior extends Behavior
{
    protected $_table;

    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->_table = $this->table();
    }

    public function moveUpEntity($tableName,$id)
    {
        $entity = $this->_table->find()->select(['id','parent_id','lft','rght'])->where([$tableName.'.id' => $id])->first();
        $this->_table->moveUp($entity);
        return $entity;
    }

    public function moveDownEntity($tableName,$id)
    {
        $entity = $this->_table->find()->select(['id','parent_id','lft','rght'])->where([$tableName.'.id' => $id])->first();
        $this->_table->moveDown($entity);
        return $entity;
    }
}

initialize()メソッド内のgetTable()メソッドを使うことで、MyCustomBehaviorが使われる際のテーブルオブジェクトを取得しています。これにより、後述の2つのメソッド内で、テーブルオブジェクトが必要となるfind()メソッドや、TreeビヘイビアのmoveDown()メソッドも問題なく使うことができています。

次に、使用したいテーブルクラスのinitializeメソッドでaddBehaviorメソッドを使ってMyCustomビヘイビアを追加します。今回はsrc/Model/Table/TasksTable.phpとなります。

TasksTable.php
class TasksTable extends Table
{
    public function initialize(array $config): void
    {
        ...

        $this->addBehavior('MyCustom');
    }
}

これで MyCustomビヘイビアの持つメソッドは、自動的にTasksTableクラスへと追加され、コントローラ内からでも呼び出すことが可能となります。

早速、src/Controller/TasksController.phpを開き、moveUp()とmoveDown()メソッドを編集します。

public function moveUp()
{
  $id = $this->request->getQuery('id');
  $upEntity = $this->Tasks->moveUpEntity('tasks',$id);
  if($this->Tasks->save($upEntity)) {
    return $this->redirect(['action' => 'detail','?' => ['id' => $upEntity->parent_id]]);
  };
}

idの取得後から、save()メソッドが実行される前までを自作のmoveUpEntity()メソッドが処理をする形となります。引数にはテーブル名と対象エンティティのidが必要となっています。これで異なるidの取得方法や、別のテーブルにおいても再利用が可能となります。

記述を終えたら、タスク詳細画面にてmove_upとmove_downを動かし、問題なければ成功です。