craftcms
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, beh
What it does
Craft CMS 5 — Extending (Plugins & Modules)
Reference for extending Craft CMS 5 through plugins and modules. Covers everything from elements and services to controllers, migrations, fields, and events.
This skill is scoped to extending Craft — building plugins, modules, custom element types, field types, and backend integrations. For site/platform development (content modeling, sections, entry types, Twig templating, plugin selection), see the craft-site skill.
Companion Skills — Always Load Together
When this skill triggers, also load:
craft-php-guidelines— PHPDoc standards, section headers, naming conventions, class organization, ECS/PHPStan, verification checklist. Required for any PHP code.ddev— All commands run through DDEV. Required for running ECS, PHPStan, scaffolding, and tests.craft-garnish— When working on CP JavaScript, asset bundles, or interactive CP components. Covers Garnish's class system, UI widgets (Modal, HUD, DisclosureMenu, Select), drag system, and the Craft.* JS class pattern.
Documentation
- Extend guide: https://craftcms.com/docs/5.x/extend/
- Class reference: https://docs.craftcms.com/api/v5/
- Generator: https://craftcms.com/docs/5.x/extend/generator.html
Use WebFetch on specific doc pages when a reference file doesn't cover enough detail.
Common Pitfalls (Cross-Cutting)
- Always use
addSelect()inbeforePrepare()— it's the Craft convention and safely additive when multiple extensions contribute columns. - Queue workers run in primary site context — use
->site('*')for cross-site queries. - Including
idingetConfig()— project config uses UIDs, never database IDs. - Business logic in models or controllers — services are where logic belongs.
- Modules need manual template root, translation, and controllerNamespace registration — nothing is automatic.
DateTimeHelperin elements/queries,Carbonin services — never mix in the same class.- Hardcoding
/adminin CP URLs —cpTriggeris configurable. UseUrlHelper::cpUrl()in PHP,cpUrl()in Twig. - Passing
$request->getBodyParams()directly tosavePluginSettings()on split-settings pages — only submitted keys persist, other settings are silently dropped. Load the full settings model first, update properties, then save.
Reference Files
Read the relevant reference file(s) for your task. Multiple files often apply together.
Task examples:
- "Build a custom element type" → read
elements.md+element-index.md+fields.md+migrations.md+cp.md - "Add a webhook endpoint" → read
controllers.md+events.md - "Create a queue job that syncs elements" → read
queue-jobs.md+elements.md+debugging.md - "Add a settings page with form fields" → read
controllers.md+cp.md+architecture.md - "Register a custom field type" → read
fields.md+events.md - "Fix PHPStan errors" → read
quality.md - "Add a dashboard widget" → read
cp.md(Dashboard Widgets) +events.md(Widget Types section) - "Expose template variables for plugin users" → read
events.md(Twig Extensions section) - "Attach custom methods to entries" → read
events.md(Behaviors section) - "Build a CP utility page" → read
events.md(Utilities section) +cp.md - "Set up Vite for a plugin's CP assets" → read
plugin-vite.md+ loadcraft-garnishskill - "Add drag-to-reorder or interactive JS to a CP page" → load
craft-garnishskill - "Write CP JavaScript for a custom field type" → read
fields.md+ loadcraft-garnishskill - "Build a headless Craft API" → read
graphql.md+ loadcraft-siteskill forheadless.md - "Configure preview for a Next.js front-end" → load
craft-siteskill forheadless.md - "Set up Pest tests for a plugin" → read
testing.md - "Write a test for a controller action" → read
testing.md - "Configure Redis for caching and sessions" → read
config-app.md - "Set up environment variables for production" → read
config-bootstrap.md - "Find a GeneralConfig setting" → read
config-general.md - "Read a config value in plugin code (App::env, parseEnv, GeneralConfig)" → read
config-bootstrap.md+config-general.md - "Check if allowAdminChanges is enabled in plugin code" → read
config-general.md+cp.md(Read-Only Mode) - "Resolve env vars in plugin settings ($MY_API_KEY)" → read
config-bootstrap.md(App::parseEnv) - "Understand CRAFT_* env var conventions" → read
config-bootstrap.md - "Configure mail transport / SMTP" → read
config-app.md - "Set up custom URL routes" → read
config-bootstrap.md - "Configure search to find short words" → read
config-app.md - "Set up GraphQL tokens and schemas" → read
graphql.md+config-general.md - "Set up caching for a high-traffic site" → read
caching.md - "Register custom permissions for my plugin" → read
permissions.md - "Check user permissions in templates" → read
permissions.md - "Set up plugin editions / feature gating" → read
architecture.md(Plugin Editions section) - "Upgrade a plugin from Craft 4 to 5" → read
quality.md(Rector section) - "Set up CI for a Craft plugin" → read
quality.md(CI/CD Integration section) - "Create sections or fields in a migration" → read
migrations.md(Content Migrations section) - "Set up database read replicas" → read
config-app.md(Database Replicas section) - "Register a module in app.php" → read
config-app.md(Module Registration section) - "Create a custom validator" → read
architecture.md(Custom Validators section) - "Create a custom filesystem type" → read
events.md(Filesystem Types section) - "Build a custom condition rule for an element index" → read
cp.md(Condition Builders section) - "Build a tri-state on/inherit/off control" → read
cp.md(CP UI Patterns — Tri-State Inheritance) - "Add tabbed settings page to a plugin" → read
cp.md(Tabbed Settings Pages) - "Show an 'overrides global' warning on a field" → read
cp.md(CP UI Patterns — Field Warning Parameter) - "What CSS variables does Craft CP use?" → read
cp.md(CP UI Patterns — Craft CSS Custom Properties) - "Set up pre-commit hooks for code quality" → read
quality.md(Pre-Commit Hooks section) - "Restrict element access by user group" → read
element-authorization.md+permissions.md - "Scope CP element index by permission" → read
element-authorization.md(Layer 3: Query Scoping) - "Add authorization events to a custom element" → read
element-authorization.md+elements.md - "Build defense-in-depth for a security plugin" → read
element-authorization.md(Defense Patterns) - "Force-logout a user from all devices" → read
sessions-and-auth.md(Plugin Patterns) - "Understand how Craft sessions work" → read
sessions-and-auth.md - "Implement password reset required" → read
sessions-and-auth.md(passwordResetRequired Gap) - "Add a column to the Users element index" → read
element-index.md(Extending Element Indexes via Events) - "Add a bulk action to an element index" → read
element-index.md(Adding a custom bulk action) - "Add a custom sidebar source to the element index" → read
element-index.md(Adding a sidebar source) - "Build a custom field type" → read
field-types-custom.md+fields.md - "Build a relation field type" → read
field-types-custom.md(Relation Fields) - "Add a condition rule to the entry index" → read
conditions.md+element-index.md - "Build a custom condition rule" → read
conditions.md - "Send email from a plugin" → read
email.md - "Register a custom system message" → read
email.md(Registering Custom System Messages) - "Configure SMTP transport" → read
config-app.md+email.md - "Deploy Craft CMS to production" → read
deployment.md - "Set up CI/CD for a Craft project" → read
deployment.md(CI/CD Patterns) - "Zero-downtime deploy" → read
deployment.md(Zero-Downtime) - "Roll back a failed deploy" → read
deployment.md(Rollback Strategies) - "Work with drafts and revisions" → read
drafts-revisions.md - "Create a draft programmatically" → read
drafts-revisions.md(Creating Drafts) - "Skip side effects for drafts in afterSave" → read
drafts-revisions.md(Plugin Considerations) - "Add generated fields to a custom element" → read
elements.md(Generated Fields) - "Customize how my element appears as a chip or card" → read
element-index.md(Element Display Modes) - "Make plugin settings read-only when allowAdminChanges is off" → read
cp.md(Read-Only Mode)
| Task | Read |
|---|---|
| Element core: lifecycle, queries, status, authorization, drafts, revisions, propagation, field layouts, events | references/elements.md |
| Element index: sources, table/card attributes, sort, conditions, actions, exporters, sidebar, metadata, extending via events (columns, sources, bulk actions, condition rules, sort) | references/element-index.md |
| Services, models, records, project config, MemoizableArray, events, API clients, custom validators | references/architecture.md |
| Controllers: CP CRUD, webhooks, API endpoints, action routing, authorization | references/controllers.md |
CP templates, form macros, admin changes, VueAdminTable, asset bundles, CP layout, permissions. For CP JavaScript interactions, also load craft-garnish skill. | references/cp.md |
| Database migrations, Install.php, foreign keys, indexes, idempotency, deployment | references/migrations.md |
| Queue jobs, BaseJob, TTR, retry, progress, batch jobs, site context | references/queue-jobs.md |
| Console commands, arguments, options, progress bars, output helpers, resave actions | references/console-commands.md |
| Debugging, performance, query strategy, profiling, Xdebug, caching, logging | references/debugging.md |
| PHPStan, ECS, code review checklist | references/quality.md |
| Testing: Pest setup, element factories, HTTP/queue/DB assertions, mocking, multi-site, console, events | references/testing.md |
| Field types, native fields, BaseNativeField, field layout elements, FieldLayoutBehavior | references/fields.md |
| Events: registration, lifecycle, naming conventions, custom events, behaviors, Twig extensions, utilities, widgets, filesystems, discovering events | references/events.md |
| GraphQL types, queries, mutations, directives, schema components, resolvers | references/graphql.md |
| Plugin Vite: VitePluginService, CP asset bundles, HMR, TypeScript, Vue in CP | references/plugin-vite.md |
| Headless & hybrid: headlessMode, GraphQL API, CORS, preview tokens, front-end frameworks | craft-site skill references/headless.md |
| GeneralConfig (system, routing, security, users, sessions, search, assets, images) | references/config-general.md |
| GeneralConfig (content, templates, performance, GC, localization, headless, GraphQL, accessibility, preview, dev, dangerous interactions) | references/config-general-extended.md |
| App config: cache, session, queue, mutex, mailer/SMTP, search, logging, CORS, DB replicas, web/console split | references/config-app.md |
| Config bootstrap: env vars, aliases, priority order, fluent API, custom.php, db.php, routes.php, htmlpurifier | references/config-bootstrap.md |
| Caching: template cache tag, data cache, static caching (Blitz), CDN, layered strategy, invalidation | references/caching.md |
| Permissions: built-in handles, user groups, custom registration, Twig/PHP checking, authorization events, strategies | references/permissions.md |
| Element authorization: four-layer defense model, authorization events, can*() methods, EVENT_BEFORE_PREPARE query scoping, controller enforcement | references/element-authorization.md |
| Sessions & auth internals: dual-layer session model, auth tokens, session invalidation, passwordResetRequired, elevated sessions, plugin patterns | references/sessions-and-auth.md |
| Custom field types: build pattern, value lifecycle, settings, input HTML, validation, search, GraphQL, relation fields | references/field-types-custom.md |
| Conditions framework: BaseCondition, ElementCondition, custom condition rules, registering rules, condition builder UI | references/conditions.md |
| Email system: system messages, custom messages, programmatic sending, templates, events, testing | references/email.md |
| Deployment: standard pipeline, project config deploy, zero-downtime, CI/CD, rollback, environment management | references/deployment.md |
| Drafts & revisions: draft types, provisional drafts, autosave, applying, merge, revisions, plugin considerations | references/drafts-revisions.md |
Plugin vs Module Differences
Plugins and modules share the same architecture patterns. The differences are in bootstrapping and registration:
| Feature | Plugin | Module |
|---|---|---|
| CP template root | Automatic (by handle) | Manual via EVENT_REGISTER_CP_TEMPLATE_ROOTS |
| Site template root | Manual via event | Same — manual for both |
| Translation category | Automatic (by handle) | Manual PhpMessageSource in init() |
| Settings model | Built-in createSettingsModel() | Env vars, config files, or private plugin (_ prefix) |
| Install migration | migrations/Install.php | Content migrations only |
| Console commands | Automatic controllerNamespace | Must set before parent::init(), must be bootstrapped |
| CP nav section | $hasCpSection = true | EVENT_REGISTER_CP_NAV_ITEMS |
| Project config | Settings auto-tracked | Manual ProjectConfig::set() only |
| Namespace alias | Automatic via Composer | Must call Craft::setAlias() |
Module Template Root Registration
use craft\events\RegisterTemplateRootsEvent;
use craft\web\View;
Event::on(View::class, View::EVENT_REGISTER_CP_TEMPLATE_ROOTS,
function(RegisterTemplateRootsEvent $event) {
$event->roots['my-module'] = __DIR__ . '/templates';
}
);
Module Translation Registration
Craft::$app->i18n->translations['my-module'] = [
'class' => \craft\i18n\PhpMessageSource::class,
'sourceLanguage' => 'en',
'basePath' => __DIR__ . '/translations',
'allowOverrides' => true,
];
Module Console Command Registration
public function init()
{
Craft::setAlias('@mymodule', __DIR__);
if (Craft::$app->getRequest()->getIsConsoleRequest()) {
$this->controllerNamespace = 'modules\\mymodule\\console\\controllers';
} else {
$this->controllerNamespace = 'modules\\mymodule\\controllers';
}
parent::init(); // MUST come after setting controllerNamespace
}
The module must be bootstrapped in config/app.php for console commands to be discoverable.
Capabilities
Install
Quality
deterministic score 0.47 from registry signals: · indexed on github topic:agent-skills · 39 github stars · SKILL.md body (14,628 chars)