Craft CMS 5 PHP coding standards covering PHPDoc blocks, class organization, naming conventions, validation patterns, ECS/PHPStan configuration, and scaffolding commands for plugin and module development.
How this skill is triggered — by the user, by Claude, or both
Slash command
/craftcms-claude-skills:craft-php-guidelinesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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).
craftcms — Architecture patterns, element lifecycle, controllers, events, migrations. Required for any Craft plugin or module development.ddev — All commands run through DDEV. Required for running ECS, PHPStan, scaffolding, and tests.When unsure about a convention, WebFetch the coding guidelines page for the authoritative answer.
addSelect() is the convention in beforePrepare() — safely additive when multiple extensions contribute columns.$_instances is not a Craft convention — private properties use underscore prefix but meaningful names like $_items, $_sections.use ...\records\MyEntity as MyEntityRecord;.ResaveElements, not ResaveElementsJob.declare(strict_types=1) is NOT used in plugin source files. Only in standalone config files like ecs.php.@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.)string|null — use ?string (short nullable notation).parent::defineRules() and you lose all inherited validation.[$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.DateTimeHelper in elements/queries, Carbon in services — never mix in the same class.@throws chains — document exceptions from called methods too, not just your own throws.$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.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.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.Read the relevant reference file(s) for your task:
| Task | Read |
|---|---|
Writing PHPDocs, @author, @since, @throws, @var, @param, type references | references/phpdoc-standards.md |
| Class structure, section headers, ordering, enums, control flow, comments, whitespace | references/class-organization.md |
| Naming classes, methods, properties, files, services, events, migrations | references/naming-conventions.md |
| CP Twig templates, form macros, translations, file headers, validation | references/templates-and-patterns.md |
| ECS, PHPStan, scaffolding commands, commit messages | references/tooling.md |
@throws chains: document every exception including uncaught from called methods.@author and @since at the bottom of class/method docblocks, after a blank line.// ========================================================================= on every class. (Craft core itself uses dash separators — // ---- — with functional/domain labels like // Statuses or // Events. The ===== separators and visibility labels below are a deliberate house convention for consistency across this project's plugins, not core style.)declare(strict_types=1) is NOT used in plugin source files — Craft's internal type coercion depends on PHP's default weak typing mode._registerCpUrlRules(), $_items.addSelect() convention in beforePrepare() — additive across extensions, prevents column conflicts.DateTimeHelper in elements/queries, Carbon in services — separate concerns prevent mixing date APIs in the same class.ddev craft make <type> --with-docblocks, then customize.ddev composer check-cs and ddev composer phpstan must pass before every commit.craftcms/ecs with SetList::CRAFT_CMS_4 preset (covers both Craft 4 and 5).?string not string|null.void return types.$foo === null, in_array($x, $y, true).(int)$foo not intval($foo).// Traits
// Const Properties
// Static Properties
// Public Properties
// Protected Properties
// Private Properties
// Public Methods
// Protected Methods
// Private Methods
Only include sections that have content. Blank line after the separator, before the first item.
else — use early returns instead.match over switch for value-mapping and returns. The official guideline is "don't use switch when a single if suffices"; switch remains acceptable where it reads more clearly (and is common in core).if statements for readability.craft\helpers\DateTimeHelper.Carbon\Carbon.[[column]] quoting in Yii2 join conditions.addSelect() in beforePrepare() — safely additive.postDate and expiryDate in addSelect() and indexed on element tables.Db::parseParam() for query parameters. Db::parseDateParam() for dates.CASCADE / SET NULL behavior.| Thing | Convention | Example |
|---|---|---|
| Services (resource) | Plural | Entries, Volumes, Users |
| Services (utility) | Domain noun | Auth, Search, Gc |
| Queue jobs | Action verb, no suffix | ResaveElements, UpdateSearchIndex |
| Records | Same name as model | Namespace distinguishes |
| Events | Three patterns | SectionEvent, RegisterUrlRulesEvent, DefineHtmlEvent |
| Element actions | Action verb, no suffix | Delete, Duplicate, SetStatus |
| Enums | PascalCase cases, string/int backed | PropagationMethod, CmsEdition |
For the complete naming reference including file structure conventions, read references/naming-conventions.md.
Before every commit:
ddev composer check-cs passesddev composer phpstan passes@throws chains verifiednpx claudepluginhub michtio/craftcms-claude-skills --plugin craftcms-claude-skillsReference for extending Craft CMS 5 via plugins and modules: elements, services, controllers, migrations, fields, events, GraphQL, and more. Auto-activates on Craft PHP code.
Enforces Drupal 10/11 coding standards using PHPCS, PHPStan, naming conventions, validation commands, PHPDoc rules, and anti-pattern fixes. Use for code reviews, static analysis, and pre-commit checks.
Craft CMS 5 development: content modeling, Twig templating, element queries, GraphQL/headless, plugins, and 4-to-5 migration.