{"id":"768472b2-94c5-4b59-921f-e74f3bb46efd","shortId":"KFUX5N","kind":"skill","title":"craft-php-guidelines","tagline":"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 b","description":"# Craft CMS 5 PHP Guidelines\n\nComplete PHP coding standards and conventions for Craft CMS 5 plugin and module development. These extend Craft's official coding guidelines with project-specific conventions.\n\n**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).\n\n## Companion Skills — Always Load Together\n\n- **`craftcms`** — Architecture patterns, element lifecycle, controllers, events, migrations. Required for any Craft plugin or module development.\n- **`ddev`** — All commands run through DDEV. Required for running ECS, PHPStan, scaffolding, and tests.\n\n## Documentation\n\n- Official coding guidelines: https://craftcms.com/docs/5.x/extend/coding-guidelines.html\n- Class reference: https://docs.craftcms.com/api/v5/\n- Generator reference: https://craftcms.com/docs/5.x/extend/generator.html\n\nWhen unsure about a convention, `WebFetch` the coding guidelines page for the authoritative answer.\n\n## Common Pitfalls\n\n- `addSelect()` is the convention in `beforePrepare()` — safely additive when multiple extensions contribute columns.\n- `$_instances` is not a Craft convention — private properties use underscore prefix but meaningful names like `$_items`, `$_sections`.\n- Records use the **same class name** as models (namespace distinguishes). Alias when importing both: `use ...\\records\\MyEntity as MyEntityRecord;`.\n- Queue jobs have **no \"Job\" suffix** — `ResaveElements`, not `ResaveElementsJob`.\n- `declare(strict_types=1)` is NOT used in plugin source files. Only in standalone config files like `ecs.php`.\n- `@author` goes on classes and methods only — never on properties.\n- Don't use `string|null` — use `?string` (short nullable notation).\n- Forget `parent::defineRules()` and you lose all inherited validation.\n- 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.\n- `DateTimeHelper` in elements/queries, `Carbon` in services — never mix in the same class.\n- Missing `@throws` chains — document exceptions from called methods too, not just your own throws.\n- 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.\n- 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`.\n- 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.\n\n## Reference Files\n\nRead the relevant reference file(s) for your task:\n\n| Task | Read |\n|------|------|\n| Writing PHPDocs, `@author`, `@since`, `@throws`, `@var`, `@param`, type references | `references/phpdoc-standards.md` |\n| Class structure, section headers, ordering, enums, control flow, comments, whitespace | `references/class-organization.md` |\n| Naming classes, methods, properties, files, services, events, migrations | `references/naming-conventions.md` |\n| CP Twig templates, form macros, translations, file headers, validation | `references/templates-and-patterns.md` |\n| ECS, PHPStan, scaffolding commands, commit messages | `references/tooling.md` |\n\n## Critical Rules\n\n1. PHPDocs on everything: classes, methods, properties. No exceptions.\n2. `@throws` chains: document every exception including uncaught from called methods.\n3. `@author` and `@since` at the bottom of class/method docblocks, after a blank line.\n4. Section headers with `// =========================================================================` on every class.\n5. `declare(strict_types=1)` is NOT used in plugin source files — Craft's internal type coercion depends on PHP's default weak typing mode.\n6. Private methods/properties prefixed with underscore: `_registerCpUrlRules()`, `$_items`.\n7. `addSelect()` convention in `beforePrepare()` — additive across extensions, prevents column conflicts.\n8. `DateTimeHelper` in elements/queries, `Carbon` in services — separate concerns prevent mixing date APIs in the same class.\n9. Always scaffold with `ddev craft make <type> --with-docblocks`, then customize.\n10. `ddev composer check-cs` and `ddev composer phpstan` must pass before every commit.\n\n## PHP Standards\n\n- Minimum PHP 8.2 (Craft CMS 5 requirement).\n- PSR-12 baseline with Craft modifications (trailing commas, constant visibility).\n- `craftcms/ecs` with `SetList::CRAFT_CMS_4` preset (covers both Craft 4 and 5).\n- Short nullable notation: `?string` not `string|null`.\n- Always specify `void` return types.\n- Typed properties everywhere. No untyped public properties.\n- Strict comparison always: `$foo === null`, `in_array($x, $y, true)`.\n- Casts over functions: `(int)$foo` not `intval($foo)`.\n\n## Section Header Order\n\n```\n// Traits\n// Const Properties\n// Static Properties\n// Public Properties\n// Protected Properties\n// Private Properties\n// Public Methods\n// Protected Methods\n// Private Methods\n```\n\nOnly include sections that have content. Blank line after the separator, before the first item.\n\n## Control Flow\n\n- **Happy path last.** Handle error conditions first with early returns.\n- **Avoid `else`** — use early returns instead.\n- **`match` over `switch`** — always.\n- **Always use curly brackets** even for single statements.\n- **Separate compound conditions** into nested `if` statements for readability.\n- **Named arguments** when calling methods with 3+ parameters.\n\n## Date Handling\n\n- **Elements and element queries**: `craft\\helpers\\DateTimeHelper`.\n- **Services** (date arithmetic): `Carbon\\Carbon`.\n- Never mix both in the same class.\n\n## Database Conventions\n\n- `[[column]]` quoting in Yii2 join conditions.\n- `addSelect()` in `beforePrepare()` — safely additive.\n- `postDate` and `expiryDate` in `addSelect()` and indexed on element tables.\n- `Db::parseParam()` for query parameters. `Db::parseDateParam()` for dates.\n- Foreign keys with explicit `CASCADE` / `SET NULL` behavior.\n\n## Naming Quick-Reference\n\n| Thing | Convention | Example |\n|-------|-----------|---------|\n| Services (resource) | Plural | `Entries`, `Volumes`, `Users` |\n| Services (utility) | Domain noun | `Auth`, `Search`, `Gc` |\n| Queue jobs | Action verb, no suffix | `ResaveElements`, `UpdateSearchIndex` |\n| Records | Same name as model | Namespace distinguishes |\n| Events | Three patterns | `SectionEvent`, `RegisterUrlRulesEvent`, `DefineHtmlEvent` |\n| Element actions | Action verb, no suffix | `Delete`, `Duplicate`, `SetStatus` |\n| Enums | PascalCase cases, string/int backed | `PropagationMethod`, `CmsEdition` |\n\nFor the complete naming reference including file structure conventions, read `references/naming-conventions.md`.\n\n## Verification Checklist\n\nBefore every commit:\n\n1. `ddev composer check-cs` passes\n2. `ddev composer phpstan` passes\n3. Tests green\n4. PHPDocs complete on all new/modified code\n5. `@throws` chains verified\n6. Section headers present and correct\n7. Imports flat alphabetical (ECS-enforced, not \"PHP globals first\")","tags":["craft","php","guidelines","craftcms","claude","skills","michtio","agent-skills","claude-code","claude-code-plugin","claude-code-skills","claude-skills"],"capabilities":["skill","source-michtio","skill-craft-php-guidelines","topic-agent-skills","topic-claude-code","topic-claude-code-plugin","topic-claude-code-skills","topic-claude-skills","topic-content-modeling","topic-craft-cms","topic-craft-cms-5","topic-craftcms","topic-ddev","topic-php","topic-twig"],"categories":["craftcms-claude-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/michtio/craftcms-claude-skills/craft-php-guidelines","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add michtio/craftcms-claude-skills","source_repo":"https://github.com/michtio/craftcms-claude-skills","install_from":"skills.sh"}},"qualityScore":"0.471","qualityRationale":"deterministic score 0.47 from registry signals: · indexed on github topic:agent-skills · 42 github stars · SKILL.md body (7,578 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T18:58:16.555Z","embedding":null,"createdAt":"2026-04-18T22:19:33.904Z","updatedAt":"2026-05-18T18:58:16.555Z","lastSeenAt":"2026-05-18T18:58:16.555Z","tsv":"'-12':661 '/api/v5/':155 '/docs/5.x/extend/coding-guidelines.html':150 '/docs/5.x/extend/generator.html':160 '1':100,238,522,567,936 '10':636 '2':531,943 '3':542,800,948 '4':556,675,680,951 '5':7,54,66,563,658,682,958 '6':588,962 '7':596,968 '8':607 '8.2':655 '9':624 'access':341,362 'across':433,602 'action':885,905,906 'addit':184,601,835 'addselect':177,597,831,840 'alia':217 'alphabet':971 'also':37 'alway':13,111,370,625,690,704,776,777 'analysi':369 'answer':174 'api':619 'app':344,352,389,391,416,417,419 'applic':415 'architectur':115 'argument':795 'arithmet':813 'array':286,708 'attr':298 'auth':880 'author':253,475,543 'authorit':173 'avoid':767 'b':51 'back':917 'base':405 'baselin':662 'beforeprepar':182,600,833 'behavior':862 'blank':554,746 'bottom':548 'bracket':780 'call':330,359,381,540,797 'callabl':285 'carbon':315,611,814,815 'cascad':859 'case':915 'cast':712 'chain':326,533,960 'check':640,940 'check-c':639,939 'checklist':932 'class':88,151,211,256,323,435,483,495,526,562,623,822 'class/method':550 'closur':289 'cms':6,29,53,65,657,674 'cmsedit':919 'code':9,59,76,146,168,957 'coercion':579 'column':189,605,825 'comma':667 'command':132,516 'comment':440,491 'commit':517,650,935 'common':175 'companion':109 'comparison':703 'complet':57,922,953 'compon':376 'compos':638,644,938,945 'compound':786 'concern':615 'condit':762,787,830 'config':249 'conflict':606 'const':432,448,724 'constant':429,456,668 'content':745 'contract':428 'contribut':188 'control':119,489,755 'convent':12,62,82,108,165,180,195,598,824,868,928 'core':83,107,293 'correct':967 'cover':49,677 'cp':503 'craft':2,5,28,47,52,64,73,106,125,194,292,378,383,388,390,413,418,575,629,656,664,673,679,808 'craft-php-guidelin':1 'craft-specif':382 'craftcm':114 'craftcms.com':149,159 'craftcms.com/docs/5.x/extend/coding-guidelines.html':148 'craftcms.com/docs/5.x/extend/generator.html':158 'craftcms/ecs':670 'critic':520 'cs':641,941 'cur':779 'custom':635 'databas':823 'date':618,802,812,854 'datetimehelp':312,608,810 'db':846,851 'ddev':46,130,135,628,637,643,937,944 'declar':97,235,446,564 'default':584 'definehtmlev':903 'definerul':275,291 'delet':910 'depend':580 'detect':444 'develop':70,129 'direct':386 'discuss':22 'distinguish':216,897 'docblock':551,633 'docs.craftcms.com':154 'docs.craftcms.com/api/v5/':153 'document':144,327,534 'domain':878 'drift':445 'duplic':427,911 'earli':765,770 'ec':41,139,513,973 'ecs-enforc':972 'ecs.php':252 'edit':19,36 'element':117,804,806,844,904 'elements/queries':314,610 'els':459,768 'enforc':974 'entri':873 'enum':488,913 'error':761 'even':33,781 'event':120,500,898 'everi':535,561,649,934 'everyth':87,525 'everywher':458,697 'exampl':869 'except':328,530,536 'expiryd':838 'explicit':348,372,858 'extend':72 'extens':187,603 'fail':367 'file':25,104,245,250,461,466,498,509,574,926 'first':753,763,978 'flat':970 'flow':490,756 'foo':705,716,719 'foreign':855 'forget':273 'form':506 'function':714 'gc':882 'generat':156 'get':358 'getconfig':392 'getset':351 'getter':349,373 'getview':353 'global':977 'goe':254 'green':950 'guidelin':4,56,77,147,169 'handl':760,803 'happi':757 'header':486,510,558,721,964 'helper':809 'hint':95 'ignor':425 'import':219,969 'includ':537,741,925 'index':842 'inherit':280 'inlin':288 'instanc':190 'instead':346,772 'int':715 'intern':577 'intval':718 'invok':308 'item':205,595,754 'job':227,230,884 'join':829 'keep':437 'key':856 'last':759 'lifecycl':118 'like':204,251 'line':426,555,747 'load':14,38,112 'local':411 'lockstep':439 'lose':278 'macro':507 'magic':339,361 'make':48,630 'match':105,773 'meaning':202 'messag':518 'method':89,258,296,302,331,385,496,527,541,735,737,739,798 'methods/properties':590 'migrat':121,501 'minimum':653 'miss':324 'mix':319,617,817 'mode':587 'model':214,895 'modif':665 'modul':32,69,128 'multipl':186,434 'must':646 'myentiti':223 'myentityrecord':225 'name':203,212,297,311,457,494,794,863,893,923 'namespac':215,896 'narrow':407 'nest':789 'never':260,318,816 'new/modified':956 'notat':272,685 'noun':879 'null':267,689,706,861 'nullabl':271,684 'offici':75,145 'order':487,722 'own':451 'ownerservic':455 'page':170 'param':479 'paramet':801,850 'parent':274 'parsedateparam':852 'parseparam':847 'pascalcas':914 'pass':363,647,942,947 'path':758 'pattern':116,900 'php':3,8,24,55,58,582,651,654,976 'phpdoc':50,85,474,523,952 'phpstan':42,140,354,393,424,441,514,645,946 'phpstan-ignore-lin':423 'pitfal':176 'plugin':30,67,102,126,243,342,350,379,572 'plural':872 'postdat':836 'prefix':200,591 'present':965 'preset':676 'prevent':604,616 'principl':84 'privat':196,431,589,732,738 'project':80 'project-specif':79 'propagationmethod':918 'properti':91,197,262,340,380,497,528,696,701,725,727,729,731,733 'protect':730,736 'psr':660 'public':304,447,700,728,734 'queri':807,849 'queue':226,883 'quick':865 'quick-refer':864 'quot':826 'read':462,472,929 'readabl':793 'record':207,222,891 'refer':152,157,453,460,465,481,866,924 'references/class-organization.md':493 'references/naming-conventions.md':502,930 'references/phpdoc-standards.md':482 'references/templates-and-patterns.md':512 'references/tooling.md':519 'regardless':92 'registercpurlrul':594 'registerurlrulesev':902 'relev':464 'requir':122,136,659 'resaveel':232,889 'resaveelementsjob':234 'resolv':357,396 'resourc':871 'return':693,766,771 'review':20 'rule':521 'run':40,133,138 'runtim':365 'safe':183,834 'scaffold':44,141,515,626 'search':881 'section':206,485,557,720,742,963 'sectionev':901 'separ':614,750,785 'servic':317,452,499,613,811,870,876 'set':343,860 'setlist':672 'setstatus':912 'short':270,683 'sinc':476,545 'singl':783 'skill':16,110 'skill-craft-php-guidelines' 'small':35 'sourc':103,244,573 'source-michtio' 'specif':81,384 'specifi':691 'standalon':248 'standard':10,60,652 'statement':784,791 'static':368,400,726 'strict':98,236,565,702 'string':266,269,295,686,688 'string/int':916 'structur':484,927 'suffix':231,888,909 'switch':775 'tabl':845 'task':470,471 'templat':505 'test':143,949 'thing':867 'three':899 'throw':325,337,477,532,959 'togeth':113 'topic-agent-skills' 'topic-claude-code' 'topic-claude-code-plugin' 'topic-claude-code-skills' 'topic-claude-skills' 'topic-content-modeling' 'topic-craft-cms' 'topic-craft-cms-5' 'topic-craftcms' 'topic-ddev' 'topic-php' 'topic-twig' 'trail':666 'trait':723 'translat':508 'true':711 'twig':504 'type':94,99,237,401,410,480,566,578,586,694,695 'uncaught':538 'underscor':199,306,593 'union':406 'unsur':162 'untyp':699 'updatesearchindex':890 'use':198,208,221,241,265,268,282,294,338,371,422,570,769,778 'user':875 'util':877 'valid':281,301,511 'validateattr':299 'validatefoo':284 'var':412,478 'verb':886,907 'verif':931 'verifi':961 'view':345 'visibl':669 'void':692 'volum':874 'weak':585 'web':414 'webfetch':166 'whitespac':492 'with-docblock':631 'write':18,473 'x':709 'y':710 'yii':307,403 'yii2':375,828","prices":[{"id":"a884f8e0-58fa-4118-a565-957f2e37cb77","listingId":"768472b2-94c5-4b59-921f-e74f3bb46efd","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"michtio","category":"craftcms-claude-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T22:19:33.904Z"}],"sources":[{"listingId":"768472b2-94c5-4b59-921f-e74f3bb46efd","source":"github","sourceId":"michtio/craftcms-claude-skills/craft-php-guidelines","sourceUrl":"https://github.com/michtio/craftcms-claude-skills/tree/main/skills/craft-php-guidelines","isPrimary":false,"firstSeenAt":"2026-04-18T22:19:33.904Z","lastSeenAt":"2026-05-18T18:58:16.555Z"}],"details":{"listingId":"768472b2-94c5-4b59-921f-e74f3bb46efd","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"michtio","slug":"craft-php-guidelines","github":{"repo":"michtio/craftcms-claude-skills","stars":42,"topics":["agent-skills","claude-code","claude-code-plugin","claude-code-skills","claude-skills","content-modeling","craft-cms","craft-cms-5","craftcms","ddev","php","twig"],"license":"mit","html_url":"https://github.com/michtio/craftcms-claude-skills","pushed_at":"2026-05-18T16:55:33Z","description":"Production-ready Claude Code skills, agents, and project templates for Craft CMS 5 development","skill_md_sha":"5ce230f0c19e9fb81a6df470cf218c66785327cc","skill_md_path":"skills/craft-php-guidelines/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/michtio/craftcms-claude-skills/tree/main/skills/craft-php-guidelines"},"layout":"multi","source":"github","category":"craftcms-claude-skills","frontmatter":{"name":"craft-php-guidelines","description":"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."},"skills_sh_url":"https://skills.sh/michtio/craftcms-claude-skills/craft-php-guidelines"},"updatedAt":"2026-05-18T18:58:16.555Z"}}