Craftcms-claude-skills craft-php-guidelines
Craft CMS 5 PHP coding standards and conventions. ALWAYS load this skill when writing, editing, reviewing, or discussing any PHP file in a Craft CMS plugin or module — even for small edits. Also load when running ECS, PHPStan, or scaffolding with ddev craft make. Covers: PHPDoc blocks (@author, @since, @throws chains, documenting exceptions), section headers (=========), class organization, naming conventions (services, queue jobs, records, events, enums), defineRules() and validation, beforePrepare() and addSelect(), MemoizableArray, DateTimeHelper vs Carbon, strict_types and declare(strict_types=1) usage, short nullable notation (?string), typed properties, void return types, control flow patterns (early returns, match over switch), CP Twig template conventions, form macros, translations (Craft::t), ECS/PHPStan configuration, scaffolding commands, and the verification checklist. Triggers on: writing service classes, models, controllers, elements, element queries, records, queue jobs, migrations, or any PHP class in a Craft CMS context. Also triggers on PHP code review, refactoring, or style questions for Craft plugins and modules. NOT for front-end Twig templates (use craft-twig-guidelines), template architecture (use craft-site), or CP JavaScript/Garnish (use craft-garnish). If you are touching PHP code in a Craft CMS context, you need this skill.
git clone https://github.com/michtio/craftcms-claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/michtio/craftcms-claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/craft-php-guidelines" ~/.claude/skills/michtio-craftcms-claude-skills-craft-php-guidelines && rm -rf "$T"
skills/craft-php-guidelines/SKILL.mdCraft CMS 5 PHP Guidelines
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).
Companion Skills — Always Load Together
— Architecture patterns, element lifecycle, controllers, events, migrations. Required for any Craft plugin or module development.craftcms
— All commands run through DDEV. Required for running ECS, PHPStan, scaffolding, and tests.ddev
Documentation
- Official coding guidelines: https://craftcms.com/docs/5.x/extend/coding-guidelines.html
- Class reference: https://docs.craftcms.com/api/v5/
- Generator reference: https://craftcms.com/docs/5.x/extend/generator.html
When unsure about a convention,
WebFetch the coding guidelines page for the authoritative answer.
Common Pitfalls
is the convention inaddSelect()
— safely additive when multiple extensions contribute columns.beforePrepare()
is not a Craft convention — private properties use underscore prefix but meaningful names like$_instances
,$_items
.$_sections- Records use the same class name as models (namespace distinguishes). Alias when importing both:
.use ...\records\MyEntity as MyEntityRecord; - Queue jobs have no "Job" suffix —
, notResaveElements
.ResaveElementsJob
is NOT used in plugin source files. Only in standalone config files likedeclare(strict_types=1)
.ecs.php
goes on classes and methods only — never on properties.@author- Don't use
— usestring|null
(short nullable notation).?string - Forget
and you lose all inherited validation.parent::defineRules()
in elements/queries,DateTimeHelper
in services — never mix in the same class.Carbon- Missing
chains — document exceptions from called methods too, not just your own throws.@throws - Using magic property access (
,$plugin->settings
) instead of explicit getters ($app->view
,$plugin->getSettings()
) — PHPStan can't resolve$app->getView()
calls, so magic access passes at runtime but fails static analysis. Always use explicit getters for Yii2 components and Craft plugin properties.__get()
Reference Files
Read the relevant reference file(s) for your task:
| Task | Read |
|---|---|
Writing PHPDocs, , , , , , type references | |
| Class structure, section headers, ordering, enums, control flow, comments, whitespace | |
| Naming classes, methods, properties, files, services, events, migrations | |
| CP Twig templates, form macros, translations, file headers, validation | |
| ECS, PHPStan, scaffolding commands, commit messages | |
Critical Rules
- PHPDocs on everything: classes, methods, properties. No exceptions.
chains: document every exception including uncaught from called methods.@throws
and@author
at the bottom of class/method docblocks, after a blank line.@since- Section headers with
on every class.// =========================================================================
is NOT used in plugin source files — Craft's internal type coercion depends on PHP's default weak typing mode.declare(strict_types=1)- Private methods/properties prefixed with underscore:
,_registerCpUrlRules()
.$_items
convention inaddSelect()
— additive across extensions, prevents column conflicts.beforePrepare()
in elements/queries,DateTimeHelper
in services — separate concerns prevent mixing date APIs in the same class.Carbon- Always scaffold with
, then customize.ddev craft make <type> --with-docblocks
andddev composer check-cs
must pass before every commit.ddev composer phpstan
PHP Standards
- Minimum PHP 8.2 (Craft CMS 5 requirement).
- PSR-12 baseline with Craft modifications (trailing commas, constant visibility).
withcraftcms/ecs
preset (covers both Craft 4 and 5).SetList::CRAFT_CMS_4- Short nullable notation:
not?string
.string|null - Always specify
return types.void - Typed properties everywhere. No untyped public properties.
- Strict comparison always:
,$foo === null
.in_array($x, $y, true) - Casts over functions:
not(int)$foo
.intval($foo)
Section Header Order
// 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.
Control Flow
- Happy path last. Handle error conditions first with early returns.
- Avoid
— use early returns instead.else
overmatch
— always.switch- Always use curly brackets even for single statements.
- Separate compound conditions into nested
statements for readability.if - Named arguments when calling methods with 3+ parameters.
Date Handling
- Elements and element queries:
.craft\helpers\DateTimeHelper - Services (date arithmetic):
.Carbon\Carbon - Never mix both in the same class.
Database Conventions
quoting in Yii2 join conditions.[[column]]
inaddSelect()
— safely additive.beforePrepare()
andpostDate
inexpiryDate
and indexed on element tables.addSelect()
for query parameters.Db::parseParam()
for dates.Db::parseDateParam()- Foreign keys with explicit
/CASCADE
behavior.SET NULL
Naming Quick-Reference
| Thing | Convention | Example |
|---|---|---|
| Services (resource) | Plural | , , |
| Services (utility) | Domain noun | , , |
| Queue jobs | Action verb, no suffix | , |
| Records | Same name as model | Namespace distinguishes |
| Events | Three patterns | , , |
| Element actions | Action verb, no suffix | , , |
| Enums | PascalCase cases, string/int backed | , |
For the complete naming reference including file structure conventions, read
references/naming-conventions.md.
Verification Checklist
Before every commit:
passesddev composer check-cs
passesddev composer phpstan- Tests green
- PHPDocs complete on all new/modified code
chains verified@throws- Section headers present and correct
- Imports alphabetical and grouped