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.
npx claudepluginhub michtio/craftcms-claude-skills --plugin craftcms-claude-skillsThis skill uses the workspace's default tool permissions.
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.
Craft CMS 5 plugin and module development — extending Craft with PHP. Covers the full extend surface: elements, element queries, services, models, records, project config, controllers, CP templates, migrations, queue jobs, console commands, field types, native fields, events, behaviors, Twig extensions, utilities, widgets, filesystems, permissions, debugging, testing, GraphQL, and Craft configuration (config/app.php, config/general.php, Redis, SMTP, database replicas). Triggers on: beforePrepare(), afterSave(), defineSources(), defineTableAttributes(), attributeHtml(), MemoizableArray, getConfig(), handleChanged, $allowAnonymous, $enableCsrfValidation, BaseNativeField, EVENT_DEFINE_NATIVE_FIELDS, FieldLayoutBehavior, EVENT_REGISTER, EVENT_DEFINE, EVENT_BEFORE, EVENT_AFTER, CraftVariable, registerTwigExtension, DefineConsoleActionsEvent, PHPStan, Pest, plugin development, module development, custom element type, custom field type, webhook, API endpoint, queue job, batch processing, data sync, migration, CP section, control panel, Craft plugin, Craft module, extending Craft, element action, element exporter, element condition, dashboard widget, utility page, permissions, registerUserPermissions, requirePermission, GraphQL custom types, GraphQL custom mutations, GraphQL schema building, Rector, Craft 4 to 5, upgrade plugin, CI/CD, GitHub Actions, GitLab CI, custom validator, defineRules, EVENT_AUTHORIZE_VIEW, EVENT_AUTHORIZE_SAVE, canView, canSave, canDelete, element authorization, defense-in-depth, query scoping, EVENT_BEFORE_PREPARE, session invalidation, passwordResetRequired, elevated session, Table::SESSIONS, custom field type build, field type development, normalizeValue, serializeValue, inputHtml, BaseCondition, ElementCondition, condition rule, condition builder, system messages, composeFromKey, email sending, Mailer, deployment, zero-downtime deploy, atomic deploy, craft up, project-config/apply, allowAdminChanges, drafts, revisions, provisional draft, canCreateDrafts, applyDraft, getIsDraft, getIsRevision, App::env, App::parseEnv, CRAFT_CP_TRIGGER, CRAFT_DEV_MODE, CRAFT_ALLOW_ADMIN_CHANGES, GeneralConfig, config precedence, cpTrigger, statusLabelHtml, Cp::statusLabelHtml, status pill, EVENT_DEFINE_ACTION_MENU_ITEMS, getActionMenuItems, disclosure menu, per-element action menu, EVENT_DEFINE_EDIT_SCREENS, DefineEditUserScreensEvent, UsersController, user edit screen, user edit tab. Always use when writing, editing, or reviewing any Craft CMS plugin or module PHP code — even when the user asks about plugin architecture, Craft internals, or extending Craft without naming specific APIs. Do NOT trigger for front-end Twig templates, content modeling decisions, site-building without PHP, or consuming GraphQL/headless APIs from front-end frameworks (Next.js, Nuxt, Astro) — those belong in craft-site.
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.
Provides Nette-specific PHP coding standards including TAB indentation, single quotes, strict_types=1, PSR-12 modifications, and use statement ordering. Invoke before writing, modifying, or refactoring any PHP code.
Share bugs, ideas, or general feedback.
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.string|null — use ?string (short nullable notation).parent::defineRules() and you lose all inherited validation.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.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.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 — always.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 verified