craft-php-guidelines について
Craft CMS 5 のプラグインやモジュール開発者向けに、公式規約を補う PHP コーディングガイドです。コード構成、型とドキュメント、日付処理、認可、ツールの規約をまとめています。
主な機能
- 01PHPDoc と例外ドキュメントの規則
- 02クラス構成と命名規約
- 03型、比較、制御フローの規則
- 04日付と環境変数の扱い
- 05認可チェックとツール要件
活用シーン
- 01Craft CMS プラグインやモジュールの開発
- 02PHP コードと静的解析の問題の確認
- 03認可チェックと日付処理の標準化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ michtio/ craftcms-claude-skills --skill craft-php-guidelinesインストール後、Claude Code で/craft-php-guidelinesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/authorization-parity.mdclass-organization.mdnaming-conventions.mdphpdoc-standards.mdtemplates-and-patterns.mdtooling.md詳細
Craft CMS 5 PHP ガイドライン
Craft CMS 5 のプラグインおよびモジュール開発に関する、PHP のコーディング標準と規約をまとめています。Craft の公式コーディングガイドラインを、プロジェクト固有の規約で補足しています。
基本原則: 型ヒントの有無にかかわらず、クラス、メソッド、プロパティのすべてに PHPDoc を記述します。Craft コアの規約に合わせ、プラグインのソースファイルでは declare(strict_types=1) を使用しません。
関連スキル — 必ず一緒に読み込む
craftcms— アーキテクチャパターン、エレメントのライフサイクル、コントローラー、イベント、マイグレーション。Craft プラグインまたはモジュールを開発する際に必須です。ddev— すべてのコマンドは DDEV 経由で実行します。ECS、PHPStan、スキャフォールディング、テストの実行に必須です。
ドキュメント
- 公式コーディングガイドライン:https://craftcms.com/docs/5.x/extend/coding-guidelines.html
- クラスリファレンス:https://docs.craftcms.com/api/v5/
- ジェネレーターのリファレンス:https://craftcms.com/docs/5.x/extend/generator.html
規約がわからない場合は、WebFetch を使ってコーディングガイドラインのページを確認してください。正式な回答を参照できます。
よくある落とし穴
beforePrepare()ではaddSelect()が慣例です。複数の拡張機能がカラムを追加しても、安全に追加できます。$_instancesは Craft の慣例ではありません。プライベートプロパティにはアンダースコアの接頭辞を付けますが、$_items、$_sectionsのように意味のある名前を使います。- レコードにはモデルと同じクラス名を使います(名前空間で区別します)。両方をインポートする場合は、
use ...\records\MyEntity as MyEntityRecord;のようにエイリアスを指定します。 - キュージョブに「Job」のサフィックスは付けません。
ResaveElementsとし、ResaveElementsJobとはしません。 - プラグインのソースファイルでは
declare(strict_types=1)を使いません。ecs.phpのような単独の設定ファイルでのみ使います。 @authorはクラスとメソッドにのみ付け、プロパティには付けません。(Craft コアではクラスレベルにのみ付けます。メソッドにも付けるのはコアのスタイルではなく、このプロジェクト独自の慣例です。)string|nullは使わず、短縮形の nullable 表記である?stringを使います。parent::defineRules()を呼び忘れると、継承されたバリデーションがすべて失われます。defineRules()で[$this, '_validateFoo']のような callable 配列やインラインクロージャーを使う方法です。Craft コアでは文字列のメソッド名を使います。[['attr'], 'validateAttr']のように指定します。バリデーターのメソッドは public とし、アンダースコアは付けません。Yii は名前で呼び出します。- 要素やクエリでは
DateTimeHelper、サービスではCarbonを使い、同じクラス内で混在させないでください。 - 生の DB 日時を
strtotime()やnew DateTime()で解析する方法です。これらのカラムはタイムゾーン情報のない UTC 文字列であり、プロセスのタイムゾーンはsystem.timeZoneなので、UTC 以外の環境では結果がタイムゾーンのオフセット分だけずれます。明示的に UTC タイムゾーンを指定して解析してください。詳しくは以下の「日付の処理」を参照してください。 @throwsの連鎖を記載し忘れないでください。自分で送出する例外だけでなく、呼び出したメソッドから送出される例外も記載してください。- 明示的なゲッター(
$plugin->getSettings()、$app->getView())ではなく、マジックプロパティアクセス($plugin->settings、$app->view)を使う方法です。PHPStan は__get()の呼び出しを解決できないため、実行時には問題なくても静的解析に失敗します。Yii2 コンポーネントと Craft プラグインのプロパティには、常に明示的なゲッターを使ってください。 - Craft 固有のメソッドを
Craft::$appに直接呼び出す方法(Craft::$app->getConfig())です。静的型が Yii の基底ユニオン型であるため、PHPStan はこれらのメソッドを解決できません。型付きのローカル変数で型を絞り込んでください。/** @var \craft\web\Application $app */ $app = Craft::$app;のようにします。@phpstan-ignore-lineは使わないでください。 - 「同期を保つ」といったコメントを添えて、複数クラスに契約定数を
private constとして重複定義する方法です。PHPStan は値の不一致を検出できません。所有するサービスでpublic constを宣言し、それ以外の箇所ではOwnerService::CONSTANT_NAMEを参照してください。これは特に権限ハンドルに当てはまります。'my-plugin:manage-settings'のようなハンドルは、登録処理(EVENT_REGISTER_PERMISSIONS)、コントローラーのゲート(requirePermission())、ナビゲーションのチェック(->can())から参照される契約文字列です。文字列リテラルを直接書くと、気付かないまま値がずれる可能性があります。また、管理者はすべての権限を持つため、タイプミスがあっても管理者には問題が現れず、それ以外の全員が拒否されます。権限を適用するコントローラーにSettingsController::PERMISSION_MANAGE_SETTINGSのようなpublic constを宣言し、すべての箇所でこの定数を参照してください。(Craft コアではここに文字列リテラルを使いますが、定数を使うのは意図的に厳格化した独自ルールです。craftcmsスキルのpermissions.mdを参照してください。) - CP コントローラー、コンソールコマンド、GraphQL リゾルバーで同じ認可チェックを別々に実装する方法です。チェック内容がずれやすく、ずれた箇所は誰にもテストされません。すべての入口から呼び出す共通のゲートメソッドを用意し、入口ごとにテストを作成してください。コンソールも例外ではありません。権限チェックのない cron 用の経路を文書化していても、それは認証されていない機能です。また、GraphQL スキーマのスコープはプラグインの権限マトリックスではありません。
references/authorization-parity.mdを参照してください。 - Craft が自己承認や自己レビューを防ぐと思い込む方法です。そのような概念はなく、ピア権限は別の軸です。ロールチェックとは独立して、明示的なバイパス権限を設けたうえで、共通ゲートにガードを実装してください。
references/authorization-parity.mdを参照してください。 - プラグインの
composer.jsonに../*のパスリポジトリを含めて配布する方法です。依存関係の解決ができるのは作成者のディスク上だけです。未公開の兄弟依存関係にはvcsエントリーを指定し、Packagist の依存関係には何も指定しません。プラグインではcomposer.lockを引き続き gitignore 対象にします。composer config --global repositoriesを実行して空であることを確認し、その後、ロックファイルなしでcomposer update --dry-runを実行して検証してください。references/tooling.md(Composer Hygiene)を参照してください。 - リテラルの比較に
Db::parseParam()を使う方法です。先頭または末尾の*が SQL のLIKEワイルドカードになるため、保存済みパターンの一意性チェックが、気付かないうちに前方一致になります。andWhere(['col' => $value])を直接使ってください。craftcmsスキルのarchitecture.mdを参照してください。 init()内のgetIsCpRequest()(またはその他のリクエストコンテキスト)による分岐内でEVENT_REGISTER_ELEMENT_TYPES/EVENT_REGISTER_FIELD_TYPESを登録する方法です。コンポーネント型の登録はすべてのコンテキスト(CP、コンソール、サイト)で実行する必要があります。そうしないと、コンソールやキューのリクエストで型がgetAllElementTypes()から消え、Gc::hardDeleteElements()がゴミ箱にある行を削除しなくなります。登録は無条件に行い、CP のレンダリングやルーティング(URL ルール、アセットバンドル、ナビゲーション)のみを条件分岐してください。craftcmsスキルのevents.md→「Registration scope」を参照してください。
参照ファイル
タスクに関連する参照ファイルを読んでください。
| タスク | 読むファイル |
|---|---|
PHPDoc、@author、@since、@throws、@var、@param、型参照の記述 | references/phpdoc-standards.md |
| クラス構造、セクション見出し、順序、列挙型、制御フロー、コメント、空白 | references/class-organization.md |
| クラス、メソッド、プロパティ、ファイル、サービス、イベント、マイグレーションの命名 | references/naming-conventions.md |
| CP Twig テンプレート、フォームマクロ、翻訳、ファイルヘッダー、バリデーション | references/templates-and-patterns.md |
| ECS、PHPStan、スキャフォールディングコマンド、プラグインリポジトリの Composer 管理、コミットメッセージ | references/tooling.md |
| CP/コンソール/GraphQL/キューの各サーフェスにおける認可の一貫性、自己参照ガード、単一の共有ゲート | references/authorization-parity.md |
重要なルール
- すべてに PHPDoc を記述します。クラス、メソッド、プロパティが対象です。例外はありません。
@throwsの連鎖では、呼び出したメソッドからスローされる未捕捉の例外も含め、すべての例外を記述します。@authorと@sinceは、空行を挟んでクラス/メソッドの DocBlock の末尾に記述します。- すべてのクラスに
// =========================================================================のセクション見出しを記述します。(Craft core 自体では、// Statusesや// Eventsのような機能/ドメインラベルとダッシュ区切りの// ----を使用します。以下の=====区切りと可視性ラベルは、このプロジェクトのプラグイン全体で一貫性を保つための独自ルールであり、core のスタイルではありません。) - プラグインのソースファイルでは
declare(strict_types=1)を使用しません。Craft 内部の型変換は、PHP のデフォルトである弱い型付けモードに依存しています。 - プライベートメソッド/プロパティにはアンダースコアを接頭辞として付けます。例:
_registerCpUrlRules()、$_items。 beforePrepare()ではaddSelect()の規約に従います。これにより、拡張機能間で追加処理が可能になり、カラムの競合を防げます。- 要素/クエリでは
DateTimeHelper、サービスではCarbonを使用します。関心事を分離することで、同じクラス内で日付 API が混在するのを防げます。 - 必ず
ddev craft make <type> --with-docblocksでスキャフォールディングしてからカスタマイズします。 - コミットの前には毎回、
ddev composer check-csとddev composer phpstanが成功しなければなりません。
PHP 標準
- PHP 8.2 以上(Craft CMS 5 の要件)。
- Craft による変更を加えた PSR-12 を基準とします(末尾のカンマ、定数の可視性)。
SetList::CRAFT_CMS_4プリセットを使用するcraftcms/ecs(Craft 4 と 5 の両方に対応)。- 短縮形の nullable 記法を使います。
string|nullではなく?string。 - 戻り値の型は常に
voidを指定します。 - すべてのプロパティに型を指定します。型指定のない public プロパティは禁止です。
- 常に厳密な比較を使用します。
$foo === null、in_array($x, $y, true)。 - 関数ではなくキャストを使用します。
intval($foo)ではなく(int)$foo。
セクション見出しの順序
// Traits
// Const Properties
// Static Properties
// Public Properties
// Protected Properties
// Private Properties
// Public Methods
// Protected Methods
// Private Methods
内容があるセクションのみを含めます。区切りの後、最初の項目の前に空行を入れます。
制御フロー
- 正常系は最後に記述します。 エラー条件を先に処理し、早期リターンを使用します。
elseは避けます。 代わりに早期リターンを使用します。- 値のマッピングや戻り値には
switchよりmatchを優先します。 公式ガイドラインは「単一のifで済む場合にswitchを使わない」です。読みやすくなる場合や core で一般的な場合は、引き続きswitchを使用できます。 - 1 文だけの場合でも必ず波括弧を使います。
- 読みやすさのため、複合条件はネストした
if文に分けます。 - 3 つ以上のパラメーターを受け取るメソッドを呼び出すときは、名前付き引数を使用します。
日付の処理
- 要素と要素クエリ:
craft\helpers\DateTimeHelper。 - サービス(日付の計算):
Carbon\Carbon。 - 同じクラス内で両方を混在させないでください。
- データベースから取得した値を解析するときは、タイムゾーンを指定してください。 日時カラムにはオフセットのない UTC の文字列が格納されます(
Db::prepareDateForDb()はオフセットなしの UTC 形式で出力します)。一方、Craft は PHP プロセスのタイムゾーンをsystem.timeZoneに設定します。そのため、UTC 以外の環境で生のカラム値に対してstrtotime()やタイムゾーンを指定しないnew DateTime()を使うと、比較結果が UTC オフセット分だけずれます。UTC 環境では正しく動作するように見えるため、この問題は見過ごされがちです。DateTimeHelper::toDateTime($value)(オフセットのない入力はデフォルトで UTC とみなされます)またはCarbon::createFromFormat('Y-m-d H:i:s', $value, 'UTC')を使用してください。詳しくは、craftcmsスキルのarchitecture.md(Record-to-Model Hydration Boundary → Those strings are naive UTC)を参照してください。
環境変数へのアクセス
getenv()ではなく、craft\helpers\App::env('VAR')を使用してください。プラグインコードと、ドキュメント内のすべての設定ファイルの例に適用されます。getenv()はスレッドセーフではなく、戻り値がstring|falseで、$_SERVERにのみ存在する値を取得しません。App::env()は Craft 独自の規約であり、'true'/'false'をブール値に正規化し、コアのドキュメントでも使われています。設定例にはuse craft\helpers\App;の行を追加してください。- 保存された値が
$VAR参照またはエイリアスの可能性がある場合(環境変数を指定できるフィールドを解決する設定モデルのゲッターなど)は、App::parseEnv()を使用してください。これは文字列全体の$VAR、またはスラッシュで区切られた…/$VAR/…のセグメントのみを解決します。5.11.0 以降では、通常のテキスト内にある$("$58 million"など)は変更されません。5.0~5.10 では解決できないトークンが文字列から削除されていたため(craftcms/cms#19522)、5.11 より前のバージョンでは、任意のユーザー向け文言をこの関数に渡さないでください。 - プロジェクトの
.envに変数を書き込む機能を提供する前に、Craft::$app->getConfig()->doesEnvVarExist('NAME')(5.11.0 以降)を使用してください。これはファイルを検索するため、「環境で設定されている」かではなく、「.envで宣言されている」かを返します。後者にはApp::env()を使用してください。
データベースの規約
- Yii2 の結合条件では、カラムを
[[column]]で引用符付けします。 beforePrepare()ではaddSelect()を使用してください。安全に追加できます。- 要素テーブルでは、
postDateとexpiryDateをaddSelect()に含め、インデックスを設定してください。 - クエリパラメーターには
Db::parseParam()を使用してください。日付にはDb::parseDateParam()を使用してください。ただし、リテラル値の比較には使用しないでください。先頭または末尾の*をLIKEのワイルドカードとして扱うため、保存された値を厳密に比較する場合は、生のandWhere(['col' => $value])を使用してください。 - 外部キーには、
CASCADE/SET NULLの動作を明示してください。
権限ハンドル
権限ハンドルはケバブケースです(handle:manage-settings)。キャメルケースは使用しないでください。 両方の部分を小文字のケバブケースにします。savepoint:manage-settings、multiplayer:take-over-field のように指定し、savepoint:manageSettings とはしません。
Craft は権限名を小文字にして userpermissions.name に保存するため、保存時に大文字と小文字の区別は失われます。manageSettings は managesettings に変わりますが、manage-settings は単語の区切りが保たれ、データベース、エクスポート、デバッグ出力でも読みやすいままです。
Craft コア独自の権限はキャメルケースです(accessCp、editUsers、viewPeerEntries)。これはプラグイン独自のハンドルと意図的に異なります。コアとの一貫性を理由に、プラグインのハンドルをキャメルケースに「修正」しないでください。また、コアのハンドルを書き換えないでください。
ハンドルを保持する PHP 定数は SCREAMING_SNAKE_CASE のままです(PERMISSION_MANAGE_SETTINGS)。ケバブケースにするのは文字列値のみです。詳しい仕組みは、craftcms スキルの permissions.md を参照してください。
命名のクイックリファレンス
| 項目 | 規則 | 例 |
|---|---|---|
| サービス(リソース) | 複数形 | Entries、Volumes、Users |
| サービス(ユーティリティ) | ドメイン名詞 | Auth、Search、Gc |
| キュージョブ | 動作を表す動詞、接尾辞なし | ResaveElements、UpdateSearchIndex |
| レコード | モデルと同じ名前 | 名前空間で区別 |
| イベント | 3つのパターン | SectionEvent、RegisterUrlRulesEvent、DefineHtmlEvent |
| エレメントアクション | 動作を表す動詞、接尾辞なし | Delete、Duplicate、SetStatus |
| 列挙型 | ケースはPascalCase、文字列または整数を基底型とする | PropagationMethod、CmsEdition |
ファイル構成の規則を含む命名規則の完全なリファレンスについては、references/naming-conventions.md をお読みください。
コピーのスタイル
ユーザー向けのコピーでは、エムダッシュ(—)やエヌダッシュ(–)を使用しないでください。対象は、フィールドラベルや説明、Craft::t() の文字列、CP の通知やフラッシュメッセージ、コンソールコマンドの出力、プラグイン/モジュールのREADME、ドキュメントです。代わりに、コンマ、ピリオド、コロン、括弧を使用してください。範囲を表す場合は、「4から10」またはASCIIハイフン(「4-10」)を使用してください。通常のハイフンは使用できます。コードコメントとPHPDocは対象外です。作業を終える前に、Craft::t() の文字列、テンプレート、ドキュメントで — と – を検索してください。(フロントエンドのTwigのコピーについては、|t 文字列とテンプレートテキストにも同じ規則を適用する craft-twig-guidelines スキルを参照してください。)
コンソールコントローラーのdocblockはターミナルヘルプです
Yiiはコンソールコントローラーのdocblockを、インラインタグを解決せずにそのままオペレーター向けヘルプとして表示します。クラスのdocblockの2行目は craft help のコマンド概要になり、最初の @tag までの説明文は help <command> の本文になります。各アクションメソッドのdocblockの1行目はそのアクションの説明となり、オプションプロパティの @var テキストは --option のヘルプになります。そのため、yii\console\Controller を継承するすべてのクラスで、次の点を守ってください。クラス/プロパティのdocblockやアクションメソッドの1行目に {@see}/{@link}/{@inheritdoc} を使わず、コマンドIDは通常のテキストで記述します。2行目に ========= のセクション見出し線を置かないでください(コマンド概要として表示されます)。オプションの @var タグでは、字下げした続きの行を使わないでください(字下げが不揃いに表示されます)。craftcms/cms の ResaveController のように、字下げなしのスタイルにそろえてください。概要文は2行目に記述します。ddev craft help <plugin> で確認してください。craft help | grep '{@' の結果が空になる必要があります。
この規則に従っていても見落としやすい、次の3点にも注意してください。
- 概要文は物理的なdocblockの2行目に置きます。最初の文という意味ではありません。
parseDocCommentSummary()(yii\console\Controller)はtrim($docLines[1])を返すだけで、それ以上の処理はしません。そのため、最初の文が3行目まで折り返されると、2行目の断片だけが表示されます(実際に「and writes」「whether every row」「prints the new」で切られた概要文がありました)。通常の折り返し幅を超えても、概要文全体を1つの物理行に収めてください。 - 2行目に生成される
* Class FooControllerは、そのままcraft helpの概要として表示されます。 リリース前に、生成ツールの定型文を実際の概要文に置き換えてください。 getHelpSummary()/getHelp()のオーバーライドがあると、docblockは無効になります。 クラスでこれらをオーバーライドすると、docblockと表示されるヘルプが黙って食い違う2つの情報源になります。また、getHelp()が概要文をそのまま返す場合、コマンドにはヘルプ本文がまったく表示されません。docblockだけを使うのが望ましいです。オーバーライドがある場合、または追加する場合は、オーバーライドで隠れる説明文を削除するか内容をそろえ、help <command>に実際の本文が表示されることを確認してください。
検証チェックリスト
コミットする前に、毎回次の項目を確認してください。
ddev composer check-csが成功するddev composer phpstanが成功する- テストがすべて成功する
- 新規/変更したすべてのコードでPHPDocsが完成している
@throwsの連鎖を確認済み- セクション見出しが適切に記載されている
- インポートがアルファベット順で統一されている(ECSで強制。「PHPのグローバルを先に」ではありません)
- ユーザー向けコピー(
Craft::t()の文字列、ラベル、CPの通知、README、ドキュメント)にエムダッシュ(—)やエヌダッシュ(–)がない