基本的には、 PSR-12 coding style guide および、 CakePHP コーディング規約 - 4.x に従って頂くものとし、加えて次のルールにも従ってください。
メソッドとメソッドの間では1行の空行を入れます。
ロウワーキャメルケースとします。
オブジェクトのプロパティ名のみ、アッパーキャメルケースとし、その他の変数は全てロウワーキャメルケースとします。
class UsersController extends AppController
{
public $Users;
private $user;
protected function getStatus()
{
$user = $this->Users->find()->first();
return $user->status;
}
}
各ファイルの最上位に記述します。
/**
* baserCMS : Based Website Development Project <https://basercms.net>
* Copyright (c) NPO baser foundation <https://baserfoundation.org/>
*
* @copyright Copyright (c) NPO baser foundation
* @link https://basercms.net baserCMS Project
* @since 5.0.0
* @license http://basercms.net/license/index.html MIT License
*/
クラスの先頭にはクラスヘッダーをつけます。
/**
* Class SitesController
*/
class SitesController extends BcAdminAppController {}
メソッドの先頭にはメソッドヘッダーをつけます。
/**
* サイト一覧を表示する
*
* @param SiteManageServiceInterface $siteManage
* @checked
* @noTodo
* @unitTest
*/
public function index(SiteManageServiceInterface $siteManage){}
クラスメソッドについては、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
*/
クラスメソッドやビューファイルの実装時、および新規ファイル追加時には、ヘッダーコメントにアノテーションでマーキングをします。
@checked : コードの精査が完了している
@noTodo : Todo が発生しない
@unitTest : unitTest が実装済である
これにより進行管理表に自動反映し、進捗状況をわかるようにしています。
なお、クラスの冒頭にアノテーションのインポートが必要となりますので忘れないようにしてください。
use BaserCore\Annotation\UnitTest;
use BaserCore\Annotation\NoTodo;
use BaserCore\Annotation\Checked;
マーキングの例
// 例)
use BaserCore\Annotation\UnitTest;
use BaserCore\Annotation\NoTodo;
use BaserCore\Annotation\Checked;
class BcBaserHelper extends Cake\View\Helper
{
/**
* コンテンツを特定する文字列を出力する
*
* URL を元に、第一階層までの文字列をキャメルケースで取得する
* ※ 利用例、出力例については BcBaserHelper::getContentsName() を参照
*
* @param bool $detail 詳細モード true にした場合は、ページごとに一意となる文字列をキャメルケースで出力する(初期値 : false)
* @param array $options オプション(初期値 : array())
* ※ オプションの詳細については、BcBaserHelper::getContentsName() を参照
* @return void
* @checked
* @noTodo
*/
なお、 baserCMS 進行管理に、メモを反映したい場合には、 Note アノテーションが利用できます。
// 例
use BaserCore\Annotation\Note;
class BcBaserHelper extends Cake\View\Helper
{
/**
* Contents Before Move
*
* @note(value="固定ページを実装するまではTODO消化できない")
*/
CakePHPのコードを修正するには、CakePHPのクラスを上書きする を参考に書き換えますが、その際、次のルールに則り、コメントによるマーキングを行ってください。
また、修正理由はできるだけ細かく記載をします。
これは、CakePHPがバージョンアップした際、baserCMSをそのCakePHPのバージョンに対応する際の重要な手がかりとなる為です。
// CUSTOMIZE ADD YYYY/MM/DD 修正者名
// 修正内容の説明
// >>>
追加コード
// <<<
// CUSTOMIZE MODIFY YYYY/MM/DD 修正者名
// 修正内容の説明
// >>>
// 元のコード
// ---
修正後のコード
// <<<
// CUSTOMIZE DELETE YYYY/MM/DD 修正者名
// 修正内容の説明
// >>>
// 元のコード
// <<<
データの変更を伴う処理について、原則 POST送信を要求するものとします。
検索を伴う処理について、原則 GET送信を要求するものとします。
マイグレーションファイルは、1テーブル1つ作成するようにしてください。
なお、マイグレーションファイルの命名規則は次のとおりです。
【命名規則】
Admin
として、Controller/Admin
フォルダに配置します。Api
として、Controller/Api
フォルダに配置します。なお、フロントエンドのコントローラーについて、プレフィックスをFront
としない理由は、プレフィックスを付与した場合、テンプレートの配置においても、templates/Front
に配置せなばならず、テーマ制作の際にわかりづらくなるためです。
BcAdminAppController
を継承します。
フォーム認証が実装され、管理画面用のテーマが適用されます。
BcFrontAppController
を継承します。
フロントページ用のテーマが適用されます。
BcApiController
を継承します。
戻り値がJSONとなります。
BcAdminApiController
を継承します。
戻り値がJSONとなり、JWT認証が実装されます。
ファットコントローラー、ファットモデルを防ぐため、ビジネスロジックはサービスに実装します。サービスの受取は、DIにより引数に注入する前提としインターフェイスを指定します。
また、コントローラーからテーブルへはできるだけ直接アクセスしないようにし、サービスを経由させるようにします。
public function edit(UsersInterface $service,)
{
$user = $service->get();
}
APIの返却値については、新規登録、更新、削除においても、基本的に対象リソースのエンティティを返却します。
また、message
と errors
も一緒に返却するようにしてください。
なお、コンテンツ管理機能を実装している場合は、対象エンティティに関連づくコンテンツエンティティも返却します。
$this->set([
'page' => $page, // 対象リソースのエンティティ
'content' => $page->content, // 関連づくコンテンツ
'message' => $message, // 通知メッセージ
'errors' => $page->getErrors() // バリデーションエラーの際の各フィールドのエラー情報
]);
$this->viewBuilder()->setOption('serialize', [
'page',
'content',
'message',
'errors'
]);
公開状態などによる表示制限は、Service クラスでなく、Controller で制御するようにします。
Service クラスではパラメーターによって処理を切り替えれるように実装します。
// コントローラーのメソッド例
// status パラメーターについてはアクセスを拒否し、publish を強制的に適用する
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, [
'conditions' => $conditions
]);
}
データベース操作時の例外では、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());
}
基本的に、Ajaxのリクエスト対象の処理は、API用のコントローラーとして作成し、JSONとして戻り値作成します。
どうしても、HTMLレンダリングの結果を戻り値として必要な場合のみ、通常のコントローラーに配置します。
テーブルはレポジトリとして利用し、ビジネスロジックはサービスクラスに実装します。
対象テーブルに関する処理だけをテーブルに実装し、外部のテーブルとの連携した処理は、サービスに実装してください。
クラスには、テーブル以外の状態(プロパティ)をできるだけ持たせず、各メソッドについて簡潔な処理となるようにします。
直接 new する事はせず、Container を経由して取得します。また、サービスの指定は、Interface を指定します。テストの場合も同様です。
DIコンテナを利用して、Interface でサービスクラスを取得する理由は、サービスクラスをいつでも他のクラスに切り替える事ができる事を目的としており、テストもそのまま利用できる事がメリットとなります。Interfaceを利用しなければそれが無意味になってしまいますので注意してください。
アクションメソッドの引数に定義します。複数定義する事ができます。
// URLパラメーターは、サービスクラスの次の引数にセットされます
// /baser/admin/baser-core/users/index/1
// 上記URLの場合、1 が $id にセットされる
public function index(
UsersServiceInterface $service,
SiteConfigsServiceInterface $siteConfigsService,
int $id)
{
}
BcContainerTrait を定義し、呼び出し箇所にて、$this->getService()
を利用します。
class Sample {
use BcContainerTrait;
public function sample()
{
$service = $this->getService(UsersServiceInterface::class);
}
}
コアプラグインについては、各プラグインではテンプレートは保有せず、全てデフォルトテーマ用のプラグイン内に配置します。
それ以外のプラグインは、各プラグインでテンプレートを保持するようにします。
テンプレート名はアクション名とできるだけ合わせます。
共通部分がある場合はエレメント化して、それぞれで読み込むようにします。
これはIDEなどで、アクション名でテンプレートを探しやすいようにするためです。
例)アクション名が add / edit の場合
ビューのテンプレートは、できるだけシンプルにするため、データの生成処理は直接書かず、コントローラーから引き渡すか、もしくはヘルパより取得します。
コントローラー内の処理が煩雑にならないよう、コントローラーの対象のサービスクラスを継承した、データ取得用のサービスクラスを準備し、一括で取得し、ビューに引き渡すようにします。
データ取得用のサービスクラスの名称は、管理画面用の場合は、末尾に AdminService
を付け、フロントエンド用の場合は、FrontService
を付けます。
// 例
UsersAdminService
UsersFrontService
取得用のメソッド名は、対象のアクション名の先頭に getViewVarsFor
を付けて統一性を保つようにします。
// ユーザー管理の新規登録画面の場合
// UsersAdminService
public function getViewVarsForAdd()
{
return [
'a' => 1,
'b' => 2
];
}
// UsersController
public function add(UsersAdminServiceInterface $adminService)
{
$this->set($adminService->getViewVarsForAdd());
}
何かしらの処理を実行し時間がかかる場合には必ずローディングを表示します。
Javascript の処理で、$.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>
データの変更を伴う処理はPOST送信とします。
リンクにおいてもデータの変更を伴う場合は、POST送信とし、postLink() を利用します。
<?= $this->BcAdminForm->postLink('削除', ['action' => 'delete'])>
フォームの中に配置する場合、フォームのネストができないため、オプションに 'block' => true
を指定し、フォームの外側に次のコードを忘れないように記述します。
<?= $this->BcAdminForm->create() ?>
<?= $this->BcAdminForm->postLink(
'削除', ['action' => 'delete'],
['block' => true]
)>
<?= $this->BcAdminForm->end() ?>
<?= $this->fetch('postLink') ?>
全ての 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 メソッドは、クラス内の全てのテストで呼び出されるので、処理時間を短縮するため、できるだけ処理を少なくします。
そのため、初期化などを行う対象は、クラス内のほとんどのメソッドから呼び出されるものだけとしてください。
一つのメソッドからしか呼び出されないようなプロパティは、テストメソッドの中で記述してください。
全てのメソッドには原則ユニットテストが必要です。
その際、テストの実装がどうしても間に合わない場合は、 markTestIncomplete()
を記載しておいてください。その際、アノテーションで、@unitTest
を付けてはいけません。
$this->markTestIncomplete('Not implemented yet.');