Skip to content
Development
Skill

/craft-php-guidelines

Craft 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,

From plugin
craftcms-claude-skills
7913 skills6 agents
Install
$ npx -y skills add michtio/craftcms-claude-skills --skill craft-php-guidelines --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/craft-php-guidelines

Context preview

The summary Claude sees to decide when to auto-load this skill.

Craft 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,

SKILL.md

craft-php-guidelines.SKILL.md
name: craft-php-guidelines
description: "Craft 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, @throws chains), section headers (=========), class organization, naming conventions (services, queue jobs, records, events, enums), defineRules() and validation, beforePrepare() and addSelect(), MemoizableArray, DateTimeHelper vs Carbon, strict_types/declare(strict_types=1), short nullable notation (?string), typed properties, void returns, control flow (early returns, match over switch), CP Twig template conventions, form macros, translations (Craft::t), ECS/PHPStan config, 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 context; PHP code review, refactoring, or style questions; requireAdmin vs requirePermission, manage-settings, settings permission, kebab-case permission handles never camelCase, allowAdminChanges, read-only settings, getCpNavItem dead nav item, permission handle constant on owning controller, App::env() never getenv(), App::parseEnv() for $VAR settings, no-em-dash user-facing copy. NOT for front-end Twig (craft-twig-guidelines), template architecture (craft-site), or CP JavaScript/Garnish (craft-garnish). If you are touching PHP in a Craft context, you need this skill."

Craft 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

  • **`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.

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

  • `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`.
  • 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** — `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.)
  • Don't use `string|null` — use `?string` (short nullable notation).
  • Forget `parent::defineRules()` and you lose all inherited validation.
  • 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.
  • `DateTimeHelper` in elements/queries, `Carbon` in services — never mix in the same class.
  • Parsing a raw DB datetime with `strtotime()` or `new DateTime()` — those columns are naive UTC strings and the process timezone is `system.timeZone`, so the result is off by the full offset on any non-UTC install. Parse with an explicit UTC zone. See Date Handling below.
  • Missing `@throws` chains — document exceptions from called methods too, not just your own throws.
  • 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.
  • 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`.
  • 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:manage-settings'` is a contract string referenced from registration (`EVENT_REGISTER_PERMISSIONS`), the controller gate (`requirePermission()`), and the nav check (`->can()`); a bare literal drifts silently and a typo passes for admins (who hold every permission) while denying everyone else. Declare it as a `public const` on the controller that enforces it — `SettingsController::PERMISSION_MANAGE_SETTINGS` — and reference the const everywhere. (Craft core uses bare literals here; the const is a deliberately stricter house rule. See the `craftcms` skill's `permissions.md`.)
  • Writing the same authorization check separately in a CP controller, a console command, and a GraphQL resolver — they drift, and the surface that drifts is the one nobody tests. One shared gate method called by ever
Read more
Ships withcraftcms-claude-skills

Production-ready Claude Code skills, agents, and project templates for Craft CMS 5 development. Built and maintained by michtio.

Get the whole plugin

Other skills on craftcms-claude-skills.