$npx -y skills add michtio/craftcms-claude-skills --skill craft-php-guidelinesCraft CMS 5 PHP coding standards and conventions. ALWAYS load when writing, editing, reviewing, or discussing any PHP in a Craft plugin or module — even small edits. Also when running ECS, PHPStan, or scaffolding with ddev craft make. Covers: PHPDoc blocks (@author, @since, @thro
| 1 | # Craft CMS 5 PHP Guidelines |
| 2 | |
| 3 | Complete PHP coding standards and conventions for Craft CMS 5 plugin and module development. These extend Craft's official coding guidelines with project-specific conventions. |
| 4 | |
| 5 | **Core principles:** PHPDocs on everything — classes, methods, and properties — regardless of type hints. No `declare(strict_types=1)` in plugin source files (matching Craft core convention). |
| 6 | |
| 7 | ## Companion Skills — Always Load Together |
| 8 | |
| 9 | - **`craftcms`** — Architecture patterns, element lifecycle, controllers, events, migrations. Required for any Craft plugin or module development. |
| 10 | - **`ddev`** — All commands run through DDEV. Required for running ECS, PHPStan, scaffolding, and tests. |
| 11 | |
| 12 | ## Documentation |
| 13 | |
| 14 | - Official coding guidelines: https://craftcms.com/docs/5.x/extend/coding-guidelines.html |
| 15 | - Class reference: https://docs.craftcms.com/api/v5/ |
| 16 | - Generator reference: https://craftcms.com/docs/5.x/extend/generator.html |
| 17 | |
| 18 | When unsure about a convention, `WebFetch` the coding guidelines page for the authoritative answer. |
| 19 | |
| 20 | ## Common Pitfalls |
| 21 | |
| 22 | - `addSelect()` is the convention in `beforePrepare()` — safely additive when multiple extensions contribute columns. |
| 23 | - `$_instances` is not a Craft convention — private properties use underscore prefix but meaningful names like `$_items`, `$_sections`. |
| 24 | - Records use the **same class name** as models (namespace distinguishes). Alias when importing both: `use ...\records\MyEntity as MyEntityRecord;`. |
| 25 | - Queue jobs have **no "Job" suffix** — `ResaveElements`, not `ResaveElementsJob`. |
| 26 | - `declare(strict_types=1)` is NOT used in plugin source files. Only in standalone config files like `ecs.php`. |
| 27 | - `@author` goes on classes and methods only — never on properties. (Craft *core* puts `@author` at the class level only; placing it on methods too is this project's house convention, not core style.) |
| 28 | - Don't use `string|null` — use `?string` (short nullable notation). |
| 29 | - Forget `parent::defineRules()` and you lose all inherited validation. |
| 30 | - Using `[$this, '_validateFoo']` callable arrays or inline closures in `defineRules()` — Craft core uses string method names: `[['attr'], 'validateAttr']`. The validator method is public, no underscore — Yii invokes it by name. |
| 31 | - `DateTimeHelper` in elements/queries, `Carbon` in services — never mix in the same class. |
| 32 | - Missing `@throws` chains — document exceptions from called methods too, not just your own throws. |
| 33 | - Using magic property access (`$plugin->settings`, `$app->view`) instead of explicit getters (`$plugin->getSettings()`, `$app->getView()`) — PHPStan can't resolve `__get()` calls, so magic access passes at runtime but fails static analysis. Always use explicit getters for Yii2 components and Craft plugin properties. |
| 34 | - Calling Craft-specific methods directly on `Craft::$app` (`Craft::$app->getConfig()`) — PHPStan can't resolve them because the static type is Yii's base union. Narrow with a typed local: `/** @var \craft\web\Application $app */ $app = Craft::$app;`. Don't use `@phpstan-ignore-line`. |
| 35 | - Duplicating contract constants as `private const` across multiple classes with "keep in lockstep" comments — PHPStan can't detect drift. Declare `public const` on the owning service, reference as `OwnerService::CONSTANT_NAME` everywhere else. This applies specifically to **permission handles**: a handle like `'my-plugin:manageSettings'` is a contract string referenced from registration (`EVENT_REGISTER_PERMISSIONS`), the controller gate (`requirePermission()`), and the nav check (`->can()`); a bar |