Lesson 11

その他の機能

Lesson 11 Chapter 1
ページネーション

ページネーションとはテーブルのデータをページごとに分けて表示する機能を言います。一度に表示するデータ量を調節し、大量のデータを一度に処理しようとすることを防げるため、動作が軽くなりパフォーマンス改善とユーザビリティ向上に役立ちます。

ページネーションを実装するには、Paginator コンポーネントとPaginator ヘルパーを利用します。それぞれに使い方を見ていきましょう。

Paginator コンポーネント

ほかのコンポーネント同様に使用するコントローラのinitialize()メソッド内にてloadComponent()メソッドを使い、'Paginator'を読み込みます。

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

使い方は、Paginatorコンポーネントの持っているpaginate()メソッドを利用します。例として Lesson7 で作成したUsersContorollerクラスのindexアクションを挙げます。

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

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

paginate()メソッドに複数のレコードが含まれるUsersのデータを渡し、$Usersに代入しています。その後はとくに何もせず$Usersをset()でビューテンプレートに送っています。つまり、paginate()メソッドを使うだけで、簡単にページネーションの設定を加えることが出来ます。

注意点として、ほかのコンポーネントであれば$this->Component->method()のように、コンポーネントからメソッドを呼び出すのが基本ですが、例外としてpaginate()メソッドはコンポーネントからではなくコントローラから直接呼び出されています。

Paginatorコンポーネントにこのような例外を持たせている理由は、paginate()メソッドを使用するとき、同クラス内に$paginateというプロパティがあった場合、その値をオプションとして参照することが出来るためです。

例えば、以下のように$paginateプロパティを同クラス内に作成し、paginate()メソッドの働きを操作することができます。src/Controller/UsersController.php に同様に記述してみましょう。

class UsersController extends AppController
{
  public $paginate = [
    'limit' => 3,
    'order' => [
      'users.created' => 'asc'
    ]
  ];
  ...

内容としては、連想配列として'limit'でレコードの表示数を3つまでとし、'order'にて、'users.created'(作成日時)の昇順を指定しています。ちなみにデフォルトの表示数は20となっています。

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

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

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

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

Paginator ヘルパー

コントローラから送られてきた paginate()メソッドで加工を行ったデータを表示する際、ビューテンプレート内でPaginator ヘルパーを使うことで、ページ間の移動を行うためのリンクを簡単に設置することができます。そのため、コンポーネントとヘルパーは基本セットで使われることとなります。

例として、 Lesson7 で作成したユーザー一覧ページにあたるtemplates/Users/index.phpのコードを挙げます。

index.php
<div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< ' . __('first')) ?>
            <?= $this->Paginator->prev('< ' . __('previous')) ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next(__('next') . ' >') ?>
            <?= $this->Paginator->last(__('last') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
    </div>

ユーザー一覧ページには以下のように表示されています。

11_1_1.png

コードの内容を見てみると、ulタグの中で$this->Paginatorから様々なメソッドを使用してリンクを設定し、最後にページに関する情報を表示させていることがわかります。それぞれのメソッドについて見ていきましょう。

  • numbers():ページ番号リンクの作成

    「1 2 3」のようにページ数でのリンクを作成します。デフォルトでは、現在のページの前後で最大8個までのリンクが作られます。ただし存在しないページは作られず、現在のページへのリンクも作られません。

    オプションは以下のものが用意されています

    • modulus:前後に表示するページ数を変更することができます。

      numbers(['modulus' => 3])
    • before / after:ページ番号リンクの左と右端に任意のテキストや記号を入れることができます

      numbers(['before' => '[','after' => ']'])
    • first / last:ページが多い場合などに、現在のページの前後とは別に最初のページと最後のページへのリンクを作成します。以下の例では、先頭から2ページと末尾から2ページのリンクを含むページリンクを生成します。

      numbers(['first' => 2, 'last' => 2])
  • 現在のページの直前/直後、ページ全体の先頭/末尾へのリンクの作成

    • first():先頭ページへのリンク

    • prev():現在のページの一つ前のページへのリンク

    • next():現在のページの一つ後のページへのリンク

    • last():最後のページへのリンク

    • これらは第一引数にリンクテキストをとります。また、先頭ページを表示しているときのfirstとprevのように、リンク先のページがなく使用できない場合には、prevとnextは自動的に使用不可となり、firstとlastはリンク自体が作成されなくなります。

  • counter():ページの情報を表示する

    ページで分割されているデータの個数を数え、ページの総数や現在のページが何ページ目なのかを表示することが出来ます。オプションで{{page}}のようなトークンと呼ばれるものを使って、表示の形式を指定することもできます。

    以下、トークンの一覧です。

    • {{page}} - 表示している現在のページ番号

    • {{pages}} - 総ページ数

    • {{current}} - 現在のページのレコード数

    • {{count}} - 全てのページを含む全レコード数

    • {{start}} - 現在のページの先頭レコードが1から数えて何番目か

    • {{end}} - 現在のページの最終レコードが1から数えて何番目か

    • {{model}} - 使用しているモデル名を表示

    startとendですが、例えば表示しているページが2ページ目で、1ページあたりの表示件数が10件である場合、{{start}} は「11」、{{end}}は「20」となります。

ソートリンクの作成

PaginatorHelperにはページリンクのほかに、ソートをかけるためのリンクを作成するsort()メソッドが備わっています。

templates/Users/index.phpのコードを見ると、ページ上部にsort()を使用しているpaginatorがあります。

index.php
<tr>
    <th><?= $this->Paginator->sort('id') ?></th>
    <th><?= $this->Paginator->sort('username') ?></th>
    <th><?= $this->Paginator->sort('created') ?></th>
    <th><?= $this->Paginator->sort('modified') ?></th>
    <th class="actions"><?= __('Actions') ?></th>
</tr>

11_1_2.png

使い方は、第一引数にソートをかけるカラム名、ページで表示するテキストを変更したい場合は第二引数で指定(指定しない場合は第一引数の値がそのまま使われます)

デフォルトでは昇順となっており、リンクを踏むたびに自動で昇順と降順でのソートを繰り返します。

また、第三引数にオプションとして direction と lockを使用することで、ソート順を固定することができます。

sort('created', null, ['direction' => 'desc','lock' => true])

例えば、上記のように設定することで、降順でのソートしかしないようにすることも出来ます。

Lesson 11 Chapter 2
ミドルウェア

ミドルウェアの考え方

ミドルウェアは、ウェブアプリケーションにおいてリクエストを処理する前に、あるいはレスポンスを返す前に、特定の処理を行うための仕組みです。

CakePHPの公式ドキュメントに、ミドルウェアを理解するうえで分かりやすい画像があるので転載します。

11_2_1.png

画像を見ると、中心にアプリケーション(APP)があり、そのまわりをRoutes、Assets、Exceptions、CORSの層が包んでいます。この「層」一つ一つがミドルウェアであり、それぞれのミドルウェアはリクエスト/レスポンスをが来た場合に処理をほどこし、その後に次の「層」へと処理を移譲していきます。

例えば、 Lesson8 で登場したAuthenticationMiddlewareなどは、リクエストが来た際に認証の結果を取得する処理を行い、その結果をリクエスト内に含めてから次のミドルウェアへと処理を移譲するという挙動をとっていました。

ほかにも、 Lesson5 で登場したCsrfProtectionMiddlewareや、この後に紹介する「エラー処理」や「ルーティングの処理」を行うミドルウェアなどがあり、それらをリレーのように順々に実行処理していくことで、アプリケーションの挙動に柔軟性を持たせ、拡張性を高めることを可能としています。

あらためてミドルウェアの使用方法についてですが、src/Application.php にある middleware()メソッド内に、使用するミドルウェアを追加することで使用可能となります。実際にApplication.phpを開いて確認してみると、以下のように $middlewareQueue に複数のミドルウェアが追加されているのが分かるかと思います。

Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            ->add(new ErrorHandlerMiddleware(Configure::read('Error')))
            
            //以下にそのほかのミドルウェア追加が続く
            ...

CakePHP で用意されているミドルウェア

ミドルウェアの概要と使用方法を見たので、CakePHPにて用意されているミドルウェアの中から主要なものをいくつか見ていきます。

  • Cake\Error\Middleware\ErrorHandlerMiddleware

    CakePHPでは例外処理が発生した場合、ErrorHandler というクラスが例外の内容に併せてエラーページのレンダリングやログの書き込みを行うのですが、ErrorHandlerだけではリクエストがアプリケーションに到達する前などで例外が起こってしまった場合などにそれらを捕捉することができません。

    そのため、リクエスト/レスポンスの送信にも干渉することのできるミドルウェアとして ErrorHandlerMiddleware を実装することで、アプリケーション内外すべての例外を捕捉することが可能となります。

    また、ErrorHandlerMiddleware が例外の捕捉と ErrorHandler の呼び出しを行い、ErrorHandler がエラーページのレンダリングやログへの入力など具体的な処理を行うように役割分担をすることで、コードの保守性を高めることにもつながります。

  • Cake\Routing\Middleware\RoutingMiddleware

    CakePHPにおいて、特定のURLパターンに対応するコントローラとアクションを指定する「ルーティング」は、config/routes.phpにて行われます。ただし、ルーティングの処理はリクエストがコントローラに達してからでは遅いため、RoutingMiddleware によってリクエストの処理中にconfig/routes.php で指定されたルーティングを実行できるようにしています。

  • Cake\I18n\Middleware\LocaleSelectorMiddleware

    ブラウザーによって送られる Accept-Language ヘッダーを取得し、アプリケーション内で使用する言語を自動的に切り替えることを可能にします。次の Chapter3 での多言語対応でも使用します。

  • Cake\Http\Middleware\CsrfProtectionMiddleware

    CSRF対策として、トークンの作成は基本的にFormヘルパーが行いますが、フォーム送信時のリクエストにおいて、セッションに保存されたトークンとフォームに付与されたトークンが一致するかを確認するのはCsrfProtectionMiddlewareの役割となります。

いずれのミドルウェアもリクエスト/レスポンス処理時のタイミングで必要な処理を行う機能を持っており、逆にリクエスト/レスポンス処理内で行う必要のない部分についてはミドルウェアとは切り離されて実装されています。

独自のミドルウェアの作成

次に自作のミドルウェアを作成する方法についてです。ミドルウェアを作成する際には、まず以下の規約に従う必要があります。

  • ミドルウェアクラスのファイルは src/Middleware に置く
  • ミドルウェアクラス名は語尾に Middlewareをつける
  • ミドルウェアは、 Psr\Http\ServerMiddlewareInterface` を実装させる

以下で、リクエストのログを出力する簡単なミドルウェアを作成し、完成までの流れを見ていきます。

ミドルウェアクラスの作成

自作ミドルウェアをsrc/Middlware/RequestLoggerMiddleware.phpとして作成します。
<?php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Cake\Log\Log;

class RequestLoggerMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // リクエスト情報をログ出力
        Log::info('Request URI: ' . $request->getUri()->getPath(),'request');
        Log::info('Request Method: ' . $request->getMethod(),'request');
        Log::info('Request Query: ' . json_encode($request->getQueryParams()),'request');
        Log::info('Request Body: ' . json_encode($request->getParsedBody()),'request');

        // 次のミドルウェアまたはアプリケーションを呼び出し、レスポンスを返す
        return $handler->handle($request);
    }
}

規約に従い、use文にPsr\Http\Server\MiddlewareInterfaceが入っています。
また、returnの内容として $handler->handle()を呼び出しています

return $handler->handle($request);

この処理を行うことで、次のミドルウェアへとリクエストを渡しています。

ミドルウェアの話からは逸れますが、ログを出力するために Cake\Log\Log クラスを使い、info()メソッドを使用しています。第二引数ではログの出力先を指定しますが、その値を'request'として指定しています。

今回、出力先として指定している'request'も自作のものを想定しているため、logs/request.logファイルを作成しておく必要があります。

また、以下のようにconfig/app.phpの'Log'にて、'request'の設定を作成しておく必要があります。

app.php
'Log' => [
        ...

        'request' => [
            'className' => FileLog::class,
            'path' => LOGS,
            'file' => 'request',
            'levels' => ['notice', 'info', 'debug'],
        ],

ミドルウェアの追加

作成したミドルウェアはsrc/Application.phpのuse文でクラスを呼び出し、middleware()メソッド内の$middlewareQueueにadd()メソッドを使用して追加します。

//リクエストデータを'request'ログに出力
use App\Middleware\RequestLoggerMiddleware;
$middlewareQueue
  ...

  ->add(new RequestLoggerMiddleware())
                    

以上で、作成したミドルウェアが使用できる状態となったので、アプリケーションの画面を更新する度に、リクエストデータが作成したlogs/request.log に出力されるようになります

Lesson 11 Chapter 3
多言語対応

多言語対応の概要

多言語対応は、アプリケーションのテキストを複数の言語に対応させ、ユーザーの言語環境に合わせて、適切な言語で表示されるようにします。

多言語対応をするには、翻訳をかけるテキストやメッセージを格納するためのファイルが必要です。CakePHPでは、.potファイルと.poファイルというものを使用します。

.potファイルは翻訳前のテキストのみが入っており、.poファイルは各言語ごとに作成し、翻訳前と翻訳済みのテキストがセットで入ります。

.potファイルに翻訳前のテキストを用意するには、多言語化が必要なテキストに__()メソッドを使用します。__()に渡されたテキストが自動的に.potファイルで管理されるようになります。

11_3_1.png

ここからは、__()メソッドに渡すテキストの書き方、.potファイルの作り方、言語の切り替えについて、より詳しく見ていきます。

多言語化する文字列の書き方

CakePHPではソースコード内で__()メソッドを使用することで、多言語化するテキストを指定していくことができます。

echo __('Hello');

例えば上記のように記述することで、'Hello'というテキストが翻訳対象として認識され、.potファイルで管理されることになります。

テキストに変数を含める

翻訳したいテキストに変数を含めたい場合は'{}'を使用して以下のように書くことができます。

echo __("{0}さんのランクは{1}です", ['SUZUKI', '「GOLD」']);

{}に含まれている数字は、後述の配列におけるインデックスとして対応しています。

また、オプションとして「date」「number」を使用することで、日付、数値を指定のフォーマットで出力することが可能です。以下の例を見てください。

echo __('今日は{0,date,long}、降水確率は{1,number,percent}です',[FrozenTime::now(),0.5]);

[翻訳結果例]
Today is April 23, 2023 with a 50% chance of rain.
                    

インデックスに対応する数のあとに、「date,フォーマット」、「number,フォーマット」の形を取っています。

dateの後に指定できるフォーマットには以下のものがあります。右側が出力例です。

  • short :「4/20/22」
  • medium :「Apr 20, 2022」
  • long :「April 20, 2022」
  • full :「Wednesday, April 20, 2022」

dateを使用して日付を出力した場合には国ごとの表記形式に自動で変換されます。日本であれば「2023年4月20日」が英語圏では「April 20, 2022」のようになります。

次にnumberのあとに指定できるフォーマットです。

  • integer: 小数以下を取り除く
  • currency: 通貨表記し小数点以下を取り除く
  • percent: パーセントとして数をフォーマット

先ほどの例では、percent を指定し、0.5 を50% として表示させていました。

potファイルの作成

.potファイルは手動でつくることも可能ですが、CakePHPが持つ「i18nツール」によるコマンドで作成することで、__()メソッドを使用したテキストのデータを持つ.potファイルを簡単に作成することができます。

i18nツールとはCakePHPにおいて多言語化を簡単に行うための機能をまとめたもので、__()メソッドもまたi18nツールが持つ機能の1つとなります。他にテキストの読み込み、potファイルの作成と管理などの機能を持ちます。
これら複数の機能を組み合わせ、多言語化に必要なpotファイルを簡単につくることが出来るのが次のコマンドとなります。

コマンドの実行

.potファイルを生成するコマンドは以下のように記述します。

bin\cake i18n extract

早速、コマンドライン上でtodo4配下に移動しコマンドを実行してみましょう。対話形式で進みますが、以下の画像の通りに操作していきます。

11_3_2.png

計4回の応答をする必要があり「What is the path you would like to extract?」は、多言語対応が必要なテキストの抽出をどのパスで行いますか?という内容になります。1、2回目の応答では、デフォルトでsrc/配下と、template配下から抽出を行うようにpathが入力されています。3回目の応答ではCakePHPが持っているコアライブラリーからも抽出を行うかを聞かれており、yesとすることでフラッシュやエラーで表示されるメッセージも多言語化の対象とすることができます。最後の4回目では、ほかにも抽出するパスがある場合に追加で指定することができます。

無事にコマンドが実行されると resources/locales/配下に.potファイルが作成されます。

11_3_3.png

default.potとcake.pot 2つのファイルが作成されており、default.potにはsrc/とtemplates/配下で抽出されたテキストが、cake.potにはCakePHPのコアライプラリーから抽出されたテキストが入っています。

また、potファイルの命名ですが、defaultやcakeといった部分は「ドメイン」と呼ばれ、どのpotファイルを使用して翻訳するか決定するために使用されます。そのため不用意にファイル名を変更しないようにしましょう。

potファイルの確認

作成したpotファイルを開くと上部にpotファイルの設定を行うための記述があり、下部を見ていくと以下のような記述が羅列されています。

#: ./templates/Users/view.php:22
msgid "Username"
msgstr ""

msgidには__()メソッドを使用したテキストが記述されており、msgstrは空欄となっています。一行目のファイルパスはmsgidを取得したファイルとなっています。

このように、ファイルパスとmsgid、msgstrを1セットとして多言語化するテキストを管理しています。

msgstrに翻訳後の内容を記述するのですが、potファイルはあくまでも多言語化するテキストを管理するためのファイルであり、実際にmsgstrに記述するのはpoファイルとなります。

poファイルの作成

早速 poファイルを作成していくのですが、poファイルは言語ごとに用意する必要があります。そのため、まずはresources/locales配下に言語別のフォルダを作成します。今回は日本語用のフォルダを作成しましょう。フォルダの命名は言語コードを使用する必要があるため、「ja」という名前で作成します。

11_3_4.png

つぎにjaフォルダの配下にpoファイルを作成します。命名はdefault.poとし、default.potの内容を全てコピー&ペーストしておきます。再度となりますが、defaultはドメインとして使われるため間違えないように入力しましょう。

これでファイルとフォルダの作成は完了です。default.poを開き、msgstrに翻訳テキストを記述していきます。全て記述するのは作業量が非常に多くなるため、今回はtemplates/Users/viewの一部を対象としましょう。以下は記述例です。

#: ./templates/Users/view.php:22
msgid "Username"
msgstr "ユーザーネーム"

#: ./templates/Users/view.php:26
msgid "Id"
msgstr "ID"

#: ./templates/Users/view.php:30
msgid "Created"
msgstr "作成日時"

#: ./templates/Users/view.php:34
msgid "Modified"
msgstr "編集日時"

#: ./templates/Users/view.php:38
msgid "tasks"
msgstr "タスク"

これで、poファイルの作成は完了です。

言語を切り替える

CakePHPには「LocaleSelectorMiddleware 」というミドルウェアが用意されており、ウェブブラウザから送信される Accept-Language というHTTPヘッダーの内容を取得し、多言語化対応を行うことができます。

設定は非常に簡単で、src/Application.phpにてLocaleSelectorMiddlewareを追加するのみです。以下の通りに、追加を行いましょう。

Application.php
use Cake\I18n\Middleware\LocaleSelectorMiddleware;

...

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

    // ミドルウェアを追加し、英語と日本語のみ対応する場合
    $middlewareQueue->add(new LocaleSelectorMiddleware(['en', 'ja']));

    
    // 全ての言語を受け入れる場合
    //$middlewareQueue->add(new LocaleSelectorMiddleware(['*']));
}

多言語化の確認

ここまでで、指定したテキストを自動的に多言語化対応させることが出来るようになりました。

default.poファイルにて記述した、Users/viewの翻訳テキストが問題なく出力されるか見てみましょう。

まず、viewページを開く前にキャッシュデータを削除します。翻訳内容はキャッシュに保存される仕組みのため、新しく翻訳データを作成した場合には必ずキャッシュクリアをするようにします。キャッシュクリアのコマンドは以下です。コマンドライン上で実行しておきましょう。

bin\cake cache clear _cake_core_

キャッシュをクリアできたら、ユーザー一覧ページから適当なユーザーのviewページに入りましょう。各項目がpoファイルで記述した日本語訳での表示になっているはずです。

11_3_5.png

日本語以外の表示を確認したい場合には、ブラウザの言語設定を変更します。使用するブラウザによって変更方法が異なるため、「ブラウザ名 言語設定」などで検索をしてみて下さい。

現在は日本語以外のpoファイルを作成していないため、ブラウザの使用言語を日本語以外にすると翻訳前のテキストで出力されます。これは__()メソッドが使用する翻訳が見つからない場合に元テキストを表示するためとなります。

以上が多言語対応の基本の流れとなります。多言語化するテキストを追加した場合には、再度i18nコマンドを叩き、potファイルの更新を行い、言語を追加する場合にはja以外にenなどのファイルを追加しpoファイルを追加していくことになります。

Lesson 11 Chapter 4
マイグレーション

アプリケーション開発を進めていく中で、テーブルを作成したり、必要に応じでカラムを追加したりと、データベースの構造に変更を加えていく場面は多々出てきます。このとき、SQL文を使ってデータベースに変更を加えていくことも出来ますが、例えば別の開発者が同一のデータベース構造を使いたい場合や、本番用のデータベースに開発環境で使っていたデータベースの構造をそのまま移したいとなったとき、どのようにすべきでしょうか?

再び、実行してきたSQL文を最初から全て順番に実行することでも再現は出来ますが、管理と実行に非常に手間がかかります。

このような手間を無くすための方法として「マイグレーション」があります。

マイグレーションとは

CakePHPにはデータベース構造の変更(テーブルの新規作成や、カラム名の変更など)をアプリケーション側からPHPで行う「マイグレーション」という機能が用意されています。

マイグレーションを使用してデータベースに変更を加えるようにすることで、現在のデータベースがこれまでにどのような変更を加えられてきたかを管理することが可能となり、更にはコマンドを使い、それまでに加えられてきた変更を一括で実行したりすることも出来ます。

この機能により、データベースに加えた変更を手動で管理する必要がなくなり、同一の構造を持つデータベースを生成する必要が出てきた場合にも容易に対応することが可能となります。

マイグレーションファイル

マイグレーションを行うには、事前に「マイグレーションファイル」という、データベース構造をどのように変更するかを記述したファイルを用意しておく必要があります。

例えば、既存のテーブルにカラムを追加し、新規テーブルも作成するとなった場合には、カラム追加用と、テーブル作成用で2つのマイグレーションファイルを作成する必要があります。

また、それぞれのマイグレーションファイルを特定出来るようにするため、ファイル名には「version」と呼ばれる値が付与されます。CakePHPではデフォルトで202304250915のような作成日時がファイル名の先頭に付き、これが個々のマイグレーションファイルを区別するためのversionの値として扱われます。

11_4_1.png

phinxlogテーブル

必要なマイグレーションファイルを作成し、初めてマイグレーションを実行した場合には「phinxlog」という実行履歴を管理するテーブルがデータベースに自動で生成されます。

phinxlogテーブルには、マイグレーションファイルのversionの値などが実行順序と同じ並びで保存されていきます。そのため、データベースに加えた変更を時系列で追跡することが可能となります。

ここまでが概要ですが、文字の説明だけではわかりにくい部分も多いため、以下の流れにしたがって実際にマイグレーションを行う方法をみていきましょう。

  1. マイグレーションファイルの作成

  2. 実行するマイグレーションの確認

  3. マイグレーションの実行

  4. 実行内容の確認、または取り消し

マイグレーションファイルの作成

バックアップの作成

マイグレーションファイルを作成していくにあたって、今回はすでにいくつかのテーブルを作成してしまっているため、現在のデータベース構成のバックアップとしてダンプファイルを生成しておきます。

ダンプファイルとは、指定したデータベースが持つ構成やテーブル、レコードデータなど一式を .dump という形式のファイルにまとめて出力したものを指します。

マイグレーションのように、変更の履歴を追ったりすることはできませんが、現状のデータベースの状態をそのまま保存しておきたい場合に有用です。

ダンプファイルの作成はコマンドライン上で、以下のように入力すると、現在のディレクトリ内にダンプファイルを生成してくれます。

注意してほしいこと

ダンプファイル作成のコマンド実行は MySQL 上のコマンドラインではないので注意してください。

mysqldump -u root -p todo > ファイル名.dump

mysqldump コマンドを使用し、todo というデータベースから任意のファイル名でダンプファイルを生成するように指示しています。

ダンプファイルの名前に決まりはありませんが、日時やデータベース名を入れるなど、管理しやすいようにしておきましょう。(202306241530_todo.dump など)

これで、仮にデータベースの操作中にデータが消えてしまった場合でも、以下のコマンドでデータを復元することができます。

mysql -u root -p todo 
                    
                    

次に、現在のデータベースの構成を管理するための、最初のマイグレーションファイルを作成します。

データベースが空の状態からマイグレーションを行なっている場合は作成する必要はありませんが、今回は途中からマイグレーションを導入する形になるため、以下の bake コマンドを実行します。

bin/cake bake migration_snapshot Initial

無事にファイルが生成されると、config/Migrations 配下に (作成日時)_initial.php が生成されます。

作成までの流れ

ここからがマイグレーション運用の通常の流れとなります。既存のテーブルに対して変更を加える際に必要なマイグレーションファイルを作成していきます。

initial.php の生成でも使用しましたが、CakePHPではマイグレーションファイルを作成するためのbakeコマンドが用意されています。以下にコマンドの例を出します。「Statuses」テーブルの新規作成を指示するマイグレーションファイルを生成するコマンドです。

bin\cake bake migration CreateStatuses status_id:tynyint status:string created

「status_id」、「status」、「created」というカラムを持たせ、それぞれの型も指定しています。

実行後は、config/Migrations配下に保存されます。

11_4_2.png

コマンド実行後に config/Migrations を開くと、(作成日時)_CreateStatuses.phpが生成されているので、開いて中身を確認してみましょう。

class CreateStatuses extends AbstractMigration
{
    ...

    public function change(): void
    {
        $table = $this->table('statuses');
        $table->addColumn('status_id', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('status', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->create();
    }

コマンドに記述した CreateStatuses の部分はそのままクラス名として使用されます。クラスの中には change() メソッドが用意されており、作成するテーブルの情報がphpで記述されています。内容を編集することも出来ますし、問題なければこのままマイグレーションの実行に移ることが可能です。

作成コマンド

先ほどの例では新規テーブルを作成するためのマイグレーションファイルを生成しましたが、他にもカラム追加用など用途に合わせた bakeコマンドの書き方があるので紹介をしていきます。

  • テーブルの新規作成

    Createテーブル名 + カラム定義

    bin\cake bake migration CreateStatuses status_id:tynyint status:string created
  • テーブルの削除

    Dropテーブル名

    bin\cake bake migration DropStatuses
  • カラム追加

    Addカラム名Toテーブル名 + カラム定義

    bin\cake bake migration AddUserIdToStatuses user_id:int
  • カラム削除

    Removeカラム名Fromテーブル名 + カラム名

    bin\cake bake migration RemoveUserIdFromStatuses user_id

カラム定義部分についてですが、「カラム名:フィールドタイプ」という形で指定しています。

カラム名がcreated(作成日時)やmodified(編集日時)もしくは updated であれば、フィールドタイプを指定せずとも自動でdatetime型が設定されますが、基本的にはフィールドタイプまで記述します。

フィールドタイプの後に [文字数] をつけることで文字数の制限を、「?」をつけると null を許容するようにできます。以下、カラム定義部分の例を挙げます。

name:string[30]    //nameカラム string型 文字数30

user_id:int?    //user_idカラム int型 null許容

また、コマンドで空のマイグレーションファイルを作成することも出来ます。ファイルを直接編集してマイグレーションファイルを作成したい場合に使えます。

bin\cake migrations create ファイル名

最後に、1つマイグレーションファイルを追加で作成しておきましょう。tasksテーブルにstatus_idカラムを追加する内容です。

bin\cake bake migration AddStatusIdToTasks status_id:integer?

コマンドライン上で実行のうえ、config/MigrationsにAddStatusIdToTasks.phpが作成されていることを確認しておきましょう。

実行コマンド

マイグレーションファイルの作成が出来たので、早速マイグレーションの実行を行いたいところですが、その前に実行されるマイグレーションファイルを確認してみましょう。

bin\cake migrations status

上記のステータスコマンドを実行すると以下のようにstatusが「down」となっている2つのデータを確認できます。それぞれのIDとNameを見ると、先ほど作成したマイグレーションファイルの内容となっています。

11_4_3.png

downの表記は、未実行のマイグレーションファイルということを表しており、実行されると「up」の表記となります。initial については現状の構造を管理しているため、自動的に実行された状態となっています。

注意してほしいこと

この後に説明をするロールバックなどで、initial を down の状態にしてしまうと、データベース上のテーブルとレコードが全て消えてしまいます。再度 UP の状態にしても、レコードの情報は復元することができないので注意してください。仮に消してしまった場合はダンプファイルで復元しましょう。

次のマイグレーション実行コマンドでは、downとなっている全てのマイグレーションファイルが実行されます。

bin/cake migrations migrate

実行が完了すると、以下のような表示となります。

11_4_4.png

これでマイグレーションの実行が出来たので、データベースで確認を行いましょう。MySQLのコマンドライン上で以下を入力してください。

show tables;

11_4_5.png

新たにphinxlogとstatusesテーブルが追加されていることがわかります。
また、以下のSQLでそれぞれのテーブルのカラムとその型も確認しておきましょう。tusksにはstatus_idを追加しています。

show columns from statuses;
show columns from tasks;

問題なく、カラムの作成と設定が出来ていればマイグレーション成功となります。ここで、phinxlogテーブルの内容もセレクト文で確認しておきましょう。

select * from phinxlog;

実行すると、phinxlogが「version」「migration_name」のカラムを持ち、実行したマイグレーションファイルを管理していることが分かるかと思います。

ここで再度、マイグレーションのステータス確認をしてみましょう。アプリケーションのサーバで以下のコマンドを入力します。

bin\cake migrations status

実行すると、全て「up」の表記となっていると思います。

11_4_6.png

これはconfig/Mugrations配下にマイグレーションファイルがあり、かつphinxlogテーブルにデータが入っているものを「up」として表示しています。

つまりはphinxlogテーブルにデータがあるかないかを元に実行済みかどうかを判断しているということになります。

次に、実行したマイグレーションを実行前の状態に戻す「ロールバック」を実行してみましょう。

bin/cake migrations rollback

実行後に、再度ステータスコマンドを入力してみると、以下のように「AddStatusIdToTasks」のみ down になっていることがわかります。

11_4_7.png

もちろん、tasksテーブルからstatus_idカラムは無くなっており、phinxlogテーブルから「AddStatusIdToTasks」分のレコードも消えています。

ここから更にロールバックを行うと、「CreateStatuses」分のマイグレーションが取り消されますし、マイグレーション実行を行えば、再び全て実行済みである UP の状態になります。

また、マイグレーション実行とロールバックにはそれぞれ「-t」というオプションを付けることが出来ます。

bin/cake migrations migrate -t 20230430081312

上記のように使用し、指定したversionまでを実行済みの状態にすることができます。逆に指定したversion以降のマイグレーションは行われません。

ロールバックの場合は指定したversionの直前までを未実行の状態に戻すことが出来ます。特定の状態まで一気にロールバックさせたい時に有用です。

このように、ステータスの確認と実行・ロールバックを使って、データベース構造のバージョンを自由に行き来することが可能です。

Lesson 11 Chapter 5
依存性の注入(DI)

DIの考え方

まず DI とは「Dependency Injection」(依存オブジェクト注入)の略であり、プログラミングの書き方(パターン)のことを指しています。

DIのパターンに従った書き方とは、その名前にもある「依存しているオブジェクト」の呼び出し方に決まりがあり、依存の度合を下げるという目的を持ちます。言葉だけでは非常にわかりにくいので、コードと共に解説していきます。

まず、依存しているとはどういう状況なのかについてです。

use .../.../B

  class A
{
  private $B;

  public function __construct()
  {
      $this->B = new B();
  }

  public function run()
  {
      $this->B->doSomething();
  }
  ...
}

class B
{
  public function doSomething()
  {
      ...
  }
}

$classA = new A();
$classA->run();

上記のクラスAでは、クラスB の doSomething()メソッドを使用するために 「__construct() の中」で クラスB のインスタンスを new で生成しています。

クラスAを使用するためにインスタンス化した場合には、必ずコンストラクタ内でクラスBが必要となるため、クラスAはクラスBが無ければ使用できない状態となります。この状態をクラスA がクラスB に依存していると言います。

DIの訳にある「依存オブジェクト」とはここで言うクラスBにあたり、このクラスB をクラスAの中で生成するのではなく、外部から注入するようにするのが DI のパターンとなります。

外部というのが何を指すのか分かりにくいので、先ほどのコードをDIのパターンを使用した状態に書き換えてみます。

use .../.../B

class A
{
  private $B;

  public function __construct(B $B)
  {
      $this->B = $B;
  }

  public function run()
  {
      $this->B->doSomething();
  }
  ...
}
  
class B
{
  public function doSomething()
  {
      ...
  }
}

$classA = new A(new B());
$classA->run();

クラスAのコンストラクタで使用していたnewが無くなり、代わりにコンストラクタの引数に(B $B)と記述しています。これはPHPのタイプヒントを使用した書き方で、Bクラスのインスタンスが代入されている変数が引数に必要であることを示しています。

この状態でクラスA のインスタンスを取得しようとした場合、上記のコードの最後にあるように、$classA = new A(new B());という形を取ります。

つまりは、クラスBのインスタンスがクラスAの中ではなく、引数として外側から注入される形となります。

このように CakePHP で DI を使用してコードを記述する場合には、依存オブジェクトを引数として外側から注入する形をとります。

ただ、現状ではクラスAを使用するにあたって、クラスBが必要であることに変わりなく、DIの目的の一つである「依存の度合を下げる(密結合から疎結合にする)目的」が果たせていません。

そのため、以下のように、依存オブジェクトをインターフェイスとして注入する方法がよく使用されます。

class A
{
  private $B;

  public function __construct(BInterface $B)
  {
      $this->B = $B;
  }

  public function doSomething()
  {
      $this->B->doSomething();
  }

  ...
}

interface BInterface
{
    public function doSomething();
}
  
class B implements BInterface
{
  public function doSomething()
  {
      ...
  }
  ...
}

class S implements BInterface
{
  public function doSomething()
  {
      ...
  }
  ...
}

クラスAのコンストラクタの引数が(BInterface $B)となり、新たにインターフェースとして用意した BInterface が追加されています。

クラスAのコンストラクタ引数が BInterface となったことで、たとえどんなクラスのインスタンスでも BInterface を実装してさえいれば、クラスAのコンストラクタ引数に取ることが可能となりました。

上記のコードでは、クラスBと、新たに追加したクラスS の両方がBInterface を implements で実装済みのため、どちらのインスタンスもクラスAに渡すことが可能となっています。

//以下のどちらでもクラスAの使用が可能となる
$A = new A(new B());
$A = new A(new S());

この状態となることで、クラスBのインスタンスという「実体」に依存していた状態から、クラスBのメソッドを定義したインターフェイスという「抽象」に依存対象がすり替わり、クラスB以外のクラスを引数とすることもできるようになったため、DIパターンの持つ依存度合を下げて「疎結合」にするという目的を果たすことができました。

DIコンテナ

ここまで、DIパターンを使用し、依存するオブジェクトを外部から注入する方法について見てきましたが、ここで新たな問題が出てきます。

例えば、先ほどの例でクラスAのインスタンスを生成するには、以下のようにインターフェイスを実装したクラスをインスタンス化して引数に渡す必要がありました。

$A = new A(new S());

今回の例では、このように手動でも無理なく記入できる程度ですが、仮にクラスSもまた依存関係を持っており、他にも事前にインスタンス化をしておくクラスがいくつもあった場合には以下のように何度も new を重ねていく必要性が出てきます。

$A = new A(new S(new S2));

クラスAのインスタンスが欲しいだけにもかかわらず、非常に手間がかかることになってしまいます。

ここで登場するのが「DIコンテナ」と呼ばれるものになります。DIコンテナは1つのオブジェクトを呼び出す際に処理が必要な依存関係をまとめて記述し保存しておくことが出来ます。

そのため、クラスAに関係する依存関係を事前にDIコンテナに記述しておくことで、new を重ねる必要もなく容易に呼びだすことが可能となります。

CakePHPでは src/Application.php 内に services()メソッドが用意されており、ここに依存関係を記述することでDIコンテナを使用して指定のインスタンスを生成することが可能となります。以下のように services()メソッド内で、$container->add()メソッドを使用し依存関係を記述します。

Application.php
public function services(ContainerInterface $container): void
{
    $container->add(BInterface::class,S::class);
}

上記は例ですが、BInterfaceのインスタンスを呼び出すだけで、自動的に BInterfaceを実装した Sクラスのインスタンスを生成することが可能となります。

DIとDIコンテナについては少し複雑なため、説明だけで理解するのが難しいかと思います。そのため、ここからは実際にDIパターンを作成し、DIコンテナを通してインスタンスを生成するまでを説明していきます。

サービスを切り離す

今回はDIパターンを学ぶための練習として、タスク一覧画面からなんらかの通知をユーザーに送ることを想定した、仮の通知機能をDIとDIコンテナを使用して作っていきます。

まずは通知機能に必要なメソッドを持つクラスを想定し、src/Contoroller/NoticeController.php を作成します。

NoticeController.php
<?php
namespace App\Controller;
use App\Controller\AppController;

class NoticeController extends AppController
{
  public function send(){
    $type = 'mail';
    return $type;
  }
}

今回はDIの練習用のため、send()メソッドのみを実装します。正常に発動した場合は文字列「mail」を返すようにしています。

次に、TasksController.php にNoticeControllerクラスを呼び出し、送信を実行するメソッドとしてnotify()メソッドを作成します。内容は以下の通りです。

TasksController.php
//最初にuse文を追加しておきます。
use App\Controller\NoticeController;

...

public function notify()
{
  //仮の送信機能を作動させるための記述
  $noticeController = new NoticeController();
  $type = $noticeController->send();

  if($type){
    $this->Flash->success('通知が '.$type.' 送信されました');
  }else{
    $this->Flash->error('通知の '.$type.' 送信に失敗しました');
  }
  return $this->redirect(['action' => 'index']);
}

上記の状態はDIパターンを使用しておらず、メソッドの中で NoticeController のインスタンスを new で作成しています。正常に send()メソッドが実行された場合には、フラッシュによって「通知が mail 送信されました」と表記するようにしています。

後ほどDIパターンに変更しますが、まずは先にビューテンプレートを編集し、メソッドを実行できるようにします。

templates/Tasks/index.php に以下の内容を追加し、「お知らせ送信」のリンクをクリックすることで notify()メソッドを実行するようにします。

index.php
<?= $this->My->makeMyUrl('Usersページ','users/'); ?>
<div><?= $this->html->link('新規作成',['action'=>'add'])?></div>
//以下を追記します
<div><?= $this->html->link('お知らせ送信',['action'=>'notify'])?></div>

ここで一度、タスク一覧画面を開き、作成した「お知らせ通知」のリンクをクリックしてみましょう。

11_5_1.png

成功時のフラッシュが表示されていれば、問題なくsend()メソッドが実行されています。

さて、ここから本題となる「サービスを切り離す」作業を行っていきます。一度、TasksController の notify()メソッドを改めて確認します

TasksController.php
use App\Controller\NoticeController;

...

public function notify()
{
  //仮の送信機能を作動させるための記述
  $noticeController = new NoticeController();
  $type = $noticeController->send();

  if($type){
    $this->Flash->success('通知が '.$type.' 送信されました');
  }else{
    $this->Flash->error('通知の '.$type.' 送信に失敗しました');
  }
  return $this->redirect(['action' => 'index']);
}

上記の状態からDIパターンを使用し、最終的に以下の状態にしていきます。

TasksController.php
use App\Service\NoticeServiceInterface;

...

public function notify(NoticeServiceInterface $notice)
  {
    //仮の送信機能を作動させるための記述
    $type = $notice->send();

    if($type){
      $this->Flash->success('通知が '.$type.'送信されました');
    }else{
      $this->Flash->error('通知の '.$type.'送信に失敗しました');
    }
    return $this->redirect(['action' => 'index']);
  }

use文とnotify()メソッドの引数がいずれも Interface となっていることが分かるかと思います。最終的な形を確認したところで、早速「サービスを切り離す」作業の内容となる「インターフェイスとサービスの作成」を行なっていきます。

まずはインターフェイスの作成を行います。src/ 配下にserviceフォルダを作成し、その中に NoticeServiceInterface.php を作成しましょう。

11_5_2.png

NoticeServiceInterface の内容は send()メソッドのみを定義した以下を記述します。

<?php

namespace App\Service;

interface NoticeServiceInterface
{
    public function send();
}

続けて、src/service 配下に MailNoticeService を作成します。内容は以下となり、implements NoticeServiceInterface を実装するようにしています。

<?php
namespace App\Service;

class MailNoticeService implements NoticeServiceInterface
{
    public function send()
    {
      $type = 'mail';
      return $type;
    }
}

NoticeServiceInterface で定義したsend()メソッドを実装しており、内容は依存オブジェクトであった NoticeController クラスの send()メソッドと全く同じとなっており、依存オブジェクトが持っている必要な処理を「サービスとして切り分けた」形となります。

サービスを注入する

必要なサービスとインターフェースが作成できたので、TasksController.php の notify()メソッドに引数として NoticeServiceInterface のインスタンスを持つ変数を指定し、依存オブジェクトを外部から注入する形をつくります。

TasksController.php
//use App\Controller\NoticeController;
use App\Service\NoticeServiceInterface;

...

public function notify(NoticeServiceInterface $notice)
{
  //仮の送信機能を作動させるための記述
  $type = $notice->send();

  if($type){
    $this->Flash->success('通知が '.$type.'送信されました');
  }else{
    $this->Flash->error('通知の '.$type.'送信に失敗しました');
  }
  return $this->redirect(['action' => 'index']);
}

メソッドの中で行っていた new の代わりに引数に指定した変数 $notice から send()メソッドを呼び出すようにしています。

一見すると、インターフェースである NoticeServiceInterface しか呼び出しおらず、send()メソッドを実装している MailNoticeService のインスタンスが取得出来ていないため、send()メソッドの実行ができないように思えます。

実際に、このままの状態では処理を走らせても正常に作動させることはできず、この後のDIコンテナへの指示を記入することで初めて MailNoticeService のインスタンスが取得できるようになります。

サービスの管理をコンテナにまとめる

先ほど notify()メソッドの引数に指定した NoticeServiceInterface $notice が send()メソッドを実装したサービスである MailNoticeService のインスタンスとなるように、DIコンテナを使用します。

DIコンテナにインスタンスを登録するには、src/Application.php の services()メソッドに記述を行います。

今回は、NoticeServiceInterface を実装したMailNoticeServiceのインスタンスを取得できるようにするために以下のように記述を行います。use文の追記も忘れないようにしましょう。

Application.php
//DI用に追加
use App\Service\MailNoticeService;
use App\Service\NoticeServiceInterface;

...

public function services(ContainerInterface $container): void
{
    $container->add(NoticeServiceInterface::class,MailNoticeService::class);
}

このように記述することで、NoticeServiceInterface のインスタンスを求めた際に、自動で MailNoticeService のインスタンスが取得できるようになります。

ここまでできたら、再びタスク一覧画面から「お知らせ送信」のリンクをクリックし、正常に動作するかを確認しましょう。

DIパターンに変更する前と同様に、成功時のフラッシュが表示されていれば成功です。

また、もう一つ説明をしておかなければいけないのが、引数の型にインターフェイスを取った場合の挙動についてです。

public function notify(NoticeServiceInterface $notice)

上記は notify()メソッドの引数部分ですが、このように引数を「インターフェイス型 変数」という形にした場合、DIコンテナはメソッドが呼び出された時点で引数がインターフェイス型であることを認識し、自動でservices()メソッド内で定義された インターフェイスに対する具体的なインスタンスを呼び出すことができます。

そのため、メソッドを呼び出すだけで、NoticeServiceInterface の具体的なインスタンスにあたる MailNoticeService が $notice へと代入されるという結果になります。

最後に異なるサービスを作成し、MailNoticeServiceと入れ替えても問題なく通知機能が動くことを確認していきます。

src/Service/PushNoticeService.php を新たに作成し、中身を以下のようにします。

PushNoticeService.php
<?php
namespace App\Service;

class PushNoticeService implements NoticeServiceInterface
{
    public function send()
    {
      $type = 'push';
      return $type;
    }
}

次に、src/Application.php を開き、use文とservices()メソッドの内容を変更します。

Application.php
use App\Service\PushNoticeService;

...

public function services(ContainerInterface $container): void
 {
     $container->add(NoticeServiceInterface::class,PushNoticeService::class);
 }

これで、NoticeServiceInterfaceに対する具体のインスタンスが PushNoticeService となりました。設定が完了したので、再びタスク一覧画面で動作を確認してみましょう。

11_5_3.png

フラッシュに表示される文字列が、mail から push に変更されていれば、サービスの入れ替え完了です。使用するインターフェイスは変わっていないため、src/Application.php の編集のみで簡単に使用するサービスを変更できることがわかるかと思います。

DIについてまとめると、DIパターンとDIコンテナを使用することで、依存していたオブジェクトの機能をサービスとインターフェースで切り離し、必要があれば異なるサービスを使用することも可能となります。そのため、機能変更などにも対応のしやすい柔軟性を得ることにつながっていきます。