baserCMS4で利用しているCakePHP2 を CakePHP4 に移行するための方針です。
メンテナンス性の高いコードを実現するためにもご協力お願いします。
CakePHP4 から、File、Folder クラスは非推奨となり、SplFileInfo、SplFileObject の利用が推奨されていますが、
baserCMSでは利用箇所が多いため、一旦、そのまま利用してください。
メンバー変数への直接アクセスはせず、できる限りセッター、ゲッターを配置するようにしてください。
@package
は削除します。namespace を記述するようになったためです。管理画面ではツールバーで現在のサイトを切り替える事ができるようになりました。
そのサイトのデータは次のコードで取得します。
// 例)コントローラーの場合
$site = $this->getRequest()->getAttributes('currentSite');
BcAdminMiddleware
で設定しています。
baserCMS4までは、$this->request->params['Content']
で取得していましたが、次のコードに変更となりました。
$content = $this->getRequest()->getAttributes('currentContent');
$site = $this->getRequest()->getAttributes('currentSite');
BcContentsRoute
で確定し、BcFrontMiddleware
で設定しています。
Admin
として、Controller/Admin
フォルダに配置します。Api
として、Controller/Api
フォルダに配置します。なお、フロントエンドのコントローラーについて、プレフィックスをFront
としない理由は、プレフィックスを付与した場合、テンプレートの配置においても、templates/Front
に配置せなばならず、テーマ制作の際にわかりづらくなるためです。
BcAdminAppController
を継承する事で、フォーム認証が実装され、管理画面用のテーマが適用されます。
BcFrontAppController
を継承する事で、フロントページ用のテーマが適用されます。
BcApiController
を継承する事で、JWT認証が実装されます。
named パラメーターは廃止となりました。query を使います。
ファットコントローラーを防ぐため、ビジネスロジックはサービスへの移行を行います。
サービスの受取は、DIにより引数に注入する前提としインターフェイスを指定します。
// URLよりのパラメーターは、サービスクラスの次の引数にセットされる
// /baser/admin/baser-core/utilities/log_maintenance/delete
// 上記URLの場合、delete が $mode にセットされる
public function log_maintenance(
UtilitiesServiceInterface $service,
UtilitiesAdminServiceInterface $adminService,
string $mode = '')
{
}
テーブルへは直接アクセスしないようにし、サービスを経由させるようにします。
公開状態などによる表示制限は、Service クラスでなく、Controller で制御するようにします。
Service クラスではパラメーターを定義し全て受け入れるように実装します。
// コントローラーのメソッド例
public function view(ContentLinksServiceInterface $service)
{
$queryParams = $this->getRequest()->getQueryParams();
if(isset($queryParams['status'])) {
if(!$this->Authentication->getIdentity()) throw new ForbiddenException();
}
$contentLink = $service->get(
$this->request->getParam('entityId'),
array_merge(['status' => 'publish'], $queryParams)
);
$this->set(compact('contentLink'));
}
// サービスのメソッド例
public function get($id, $options = [])
{
$options = array_merge([
'status' => ''
], $options);
$conditions = [];
if($options['status'] === 'publish') {
$conditions = $this->ContentLinks->Contents->getConditionAllowPublish();
}
return $this->ContentLinks->get($id, [
'contain' => ['Contents' => ['Sites']],
'conditions' => $conditions
]);
}
Ajaxのリクエスト対象の処理は、API用のコントローラーに移行し、戻り値をJSON化してください。
どうしてもHTMLレンダリングが必要な場合のみ、Admin 用のコントローラーに配置します。
データベース操作時の例外では、PersistenceFailedException
と Throwable
を利用して、例外を漏れなくキャッチします。
BcException
では、全ての例外はキャッチできません。ただし、処理側で意図した上で BcException
を投げる場合もありますので、その際は、都度、適した例外処理を行います。
try {
// 成功
$service->create($this->getRequest()->getData());
$message = __d('baser', 'XXX を追加しました。');
} catch (PersistenceFailedException $e) {
// 入力エラーの場合、PersistenceFailedException が throw される
// $e->getEntity() で、エラー情報付きのエンティティを取得する
// 想定内のエラーとしてステータスを 400 とする
$this->setResponse($this->response->withStatus(400));
$entity = $e->getEntity();
$message = __d('baser', "入力エラーです。内容を修正してください。");
} catch (Throwable $e) {
// 何が起きているか分からない場合は、Throwable でキャッチする
// 想定外のエラーとしてステータスを 500 とする
$this->setResponse($this->response->withStatus(500));
$message = __d('baser', 'データベース処理中にエラーが発生しました。' . $e->getMessage());
}
CakePHP2系のモデルはテーブルへと移行となりますが、ファットモデルを防ぐため、
移行するメソッドのうち、できるだけ対象となるエンティティの処理だけをテーブルにまとめあげ、
外部のテーブルとの連携した処理を行うメソッドは、サービスへの移行を検討します。
引数はリクエストを直接受け取るような事をせず、シグネチャをはっきりさせ仕様を明確化します。
サービスに移行します。
クラスには、テーブル以外のデータ(プロパティ)を持たせず、各メソッドについて簡潔な処理となるようにします。
直接 new する事はせず、Container を経由して取得します。また、サービスの指定は、Interface を指定します。テストの場合も同様です。
DIコンテナを利用して、Interface でサービスクラスを取得する理由は、サービスクラスをいつでも他のクラスに切り替える事ができる事を目的としており、テストもそのまま利用できる事がメリットとなります。Interfaceを利用しなければそれが無意味になってしまいますので注意してください。
アクションメソッドの引数に定義します。複数定義する事ができます。
public function index(
UsersServiceInterface $service,
SiteConfigsServiceInterface $siteConfigsService,
int $id) {
}
BcContainerTrait を定義し、呼び出し箇所にて、$this->getService()
を利用します。
class Sample {
use BcContainerTrait;
public function sample()
{
$service = $this->getService(UsersServiceInterface::class);
}
}
コアプラグインについては、各プラグインではテンプレートは保有せず、全てテーマ内に配置します。
それ以外のプラグインは、各プラグインでテンプレートを保持するようにします。
baserCMS4までは、コントローラーのアクション名とテンプレート名が違い事がありましたが(edit の際に form を指定するなど)、できるだけ合わせるようにします。
共通箇所がある場合は、エレメント化して、それぞれで読み込むようにします。
ビューのテンプレートは、できるだけシンプルにするため、データの生成処理などを書かず、コントローラーかもしくはヘルパより取得します。
コントローラー内の処理が煩雑にならないよう、対象のサービスクラスを継承した、データ取得用のサービスクラスを準備し、一括で取得しビューに引き渡すようにしましょう。
データ取得用のサービスクラスの名称は、管理画面用の場合は、末尾に AdminService
を付け、フロントエンド用の場合は、FrontService
を付けます。
UsersAdminService
UsersFrontService
取得用のメソッド名は、先頭に getViewVarsFor
を付けて統一性を保つようにします。
getViewVarsForAdd
// ユーザー管理の新規登録画面の場合
public function add(UsersAdminServiceInterface $adminService)
{
$this->set($adminService->getViewVarsForAdd());
}
対象画面でしか利用しないようなデータは、専用のヘルパを準備します。
ヘルパの名称は、管理画面用の場合は、末尾に AdminHelper
を付け、フロントエンド用の場合は、FrontHelper
を付けます。
UsersAdminHelper
UsersFrontHelper
何かしらの処理を実行し時間がかかる場合には必ずローディングを表示します。
$.bcUtil.showLoader()
を利用することでローディングが表示できますが、対象物に bca-loading
クラスを付与する事で簡単にローディングを表示できます。
<?php echo $this->BcAdminForm->button(__d('baser', '保存'), [
'div' => false,
'class' => 'bca-btn bca-actions__item bca-loading',
'data-bca-btn-type' => 'save',
'data-bca-btn-size' => 'lg',
'data-bca-btn-width' => 'lg'
]) ?>
保存ボタンをクリックして画面遷移中にローディングを表示するだけでよいなど、ローディングの非表示処理が必要でない場合は、bca-loading
を利用してください。
検索処理は GET で実装します。また、 $this->BcAdminForm->create()
のオプションにて novalidate => true
を設定します。
ボタンのタグを次に統一します。
<div class="bca-search__btns-item">
<?php echo $this->BcAdminForm->button(__d('baser', '検索'), ['id' => 'BtnSearchSubmit', 'class' => 'bca-btn', 'data-bca-btn-type' => 'search']) ?>
</div>
<div class="bca-search__btns-item">
<?php echo $this->BcAdminForm->button(__d('baser', 'クリア'), ['id' => 'BtnSearchClear', 'class' => 'bca-btn', 'data-bca-btn-type' => 'clear']) ?>
</div>
フォームタグのフィールド定義については、モデル名を除外します。
<?php $this->BcAdminForm->control('ModelName.field_name', ['type' => 'text']) ?>
↓
<?php $this->BcAdminForm->control('field_name', ['type' => 'text']) ?>
データの変更を伴う処理はPOST送信とします。
リンクにおいてもデータの変更を伴う場合は、POST送信とします。
その際、$this->BcAdminForm->postLink()
を利用します。
フォームの中に配置する場合、フォームのネストができないため、オプションに 'block' => true
を指定し、フォームの外側に次のコードを忘れないように記述します。
<?= $this->fetch('postLink') ?>
ヘルパーにおける注意点 を参照してください。
Web API を実装するコントローラーは、認証有無に関わらず、Api
ディレクトリ配下に配置し、BcApiController
を継承します。
BcApiController
継承すると、デフォルトの状態で、JWT認証がかかった状態となり、アクセストークンが必要となります。
ログイン用のエンドポイントに対して、email と password を送信し、認証に成功すると、アクセストークンとリフレッシュトークンを取得する事ができます。
エンドポイント
/baser/api/baser-core/users/login.json
返却値
アクセストークンの付与方法は、クエリパラメーターとして付与する方法と、Authorization ヘッダに設定する方法の2種類があります。
# クエリパラメーターに付与する
/baser/api/baser-core/pages/add.json?token={アクセストークン}
# Authorizationヘッダに設定する(jQuery)
$.ajax({
headers: {"Authorization": "{アクセストークン}"},
url: '/baser/api/baser-core/pages/add.json'
})
APIの返却値については、新規登録、更新、削除においても、基本的に対象リソースのエンティティを返却します。
また、message
と errors
も一緒に返却するようにしてください。
なお、コンテンツ管理機能を実装している場合は、関連づくコンテンツも返却します。
$this->set([
'page' => $page, // 対象リソースのエンティティ
'content' => $page->content, // 関連づくコンテンツ
'message' => $message, // 通知メッセージ
'errors' => $page->getErrors() // バリデーションエラーの際の各フィールドのエラー情報
]);
$this->viewBuilder()->setOption('serialize', [
'page',
'content',
'message',
'errors'
]);
認証が不要なリソースについて、認証なしでアクセスできるようにするためには、コントローラーの initialize
にて、allowUnauthenticated としてマークします。
public function initialize(): void
{
parent::initialize();
$this->Authentication->allowUnauthenticated(['view', 'index']);
}
下書きのデータなどまだ公開したくないデータは、status
フィールドなどで公開状態を制限する前提として、
認証ありの場合は取得できるようにし、認証なしの場合は取得できないようにします。
基本的には、制限をかけず取得できるように実装します。
基本的には、デフォルトで公開状態のデータのみ取得できるように実装し、status
パラメーターの切り替えで非公開状態のものも全て取得できるようにします。
# status を空に切り替え
/baser/api/baser-core/pages.json?status=
なお、status
パラメーターの切り替えを行う際、ログイン状態を確認し、ログインしてない場合は、ForbiddenException
をスローしてください。
$queryParams = $this->getRequest()->getQueryParams();
if (isset($queryParams['status'])) {
if (!$this->Authentication->getIdentity()) throw new ForbiddenException();
}
$queryParams = array_merge([
'status' => 'publish'
], $queryParams);
$pages = $service->getIndex($queryParams);
baserCMS5は、初期状態で、RESTful なURLを自動生成する仕組みとなっています。
HTTP | URL形式 | コントローラーアクション |
---|---|---|
GET | /baser/api/baser-core/pages.json | PagesController::index() |
GET | /baser/api/baser-core/pages/123.json | PagesController::view(123) |
POST | /baser/api/baser-core/pages.json | PagesController::add() |
PUT | /baser/api/baser-core/pages/123.json | PagesController::edit(123) |
DELETE | /baser/api/baser-core/pages/123.json | PagesController::delete(123) |
ルーティングにおける注意点 を参照してください。
リクエスト関連における注意点 を参照してください。
セッション関連における注意点 を参照してください。
データベースにおける注意点 を参照してください。
プラグインにおける注意点 を参照してください。
セキュリティコンポーネントにおける注意点 を参照してください。
全ての Javascript は、画面ごとに外部ファイル化し、webpack で圧縮します。
Javascriptの作成 についてを参照してください。
全ての CSS は、画面ごとに外部ファイル化し、sass で作成してコンパイルします。
CSSの作成についてを参照してください。
既存のテストが存在する場合は、大きな仕様変更がない限り、既存のテストが動作するように調整します。
既存のテストの動作しない状態で移行する事はバグを発生させている事と同じです。
CakePHP4.3 より、フィクスチャマネージャーが非推奨になりました。
新しく作成するコードは、フィクスチャマネージャーではなく、フィクスチャファクトリーを利用して作成します。
参考:フィクスチャの利用
BcContainerTrait
を利用して、インターフェイスを指定して初期化します。
// 例
class SampleTest
{
use BcContainerTrait;
public function setUp(): void
{
parent::setUp();
$this->SampleService = $this->getService(SampleServiceInterface::class);
}
}
setUp メソッドは、クラス内の全てのテストで呼び出されるので、処理時間を短縮するため、できるだけ処理を少なくします。
そのため、初期化などを行う対象は、クラス内のほとんどのメソッドから呼び出されるものだけとしてください。
一つのメソッドからしか呼び出されないようなプロパティは、テストメソッドの中で記述してください。
そのほか、テストに関する情報はこちら
クラスメソッドについては、phpDocumentor でドキュメントを自動生成する前提とします。
マークダウン記法を前提として、次のルールに則って分かりやすいドキュメントとなるようお願いします。
###
からとします。@param
における $options
のように複数のオプションを持つ場合は、オプションキーと詳細を記述します。その際、初期値の設定がある場合は明記します。// 例
/**
* プラグインをアップロードする
*
* POSTデータにて キー`file` で Zipファイルをアップロードとすると、
* /plugins/ 内に、Zipファイルを展開して配置する。
*
* ### エラー
* post_max_size を超えた場合、サーバーに設定されているサイズ制限を超えた場合、
* Zipファイルの展開に失敗した場合は、Exception を発生。
*
* ### リネーム処理
* 展開後のフォルダー名はアッパーキャメルケースにリネームする。
* 既に /plugins/ 内に同名のプラグインが存在する場合には、数字付きのディレクトリ名(PluginName2)にリネームする。
* 数字付きのディレクトリ名にリネームする際、プラグイン内の Plugin クラスの namespace もリネームする。
*
* @param array $postData
* @param array $options
* - key: キーの名称(初期値: null)
* - excludes: 除外対象(初期値: [])
* @return string Zip を展開したフォルダ名
* @checked
* @noTodo
* @throws BcException
*/