This file provides guidance to AI Coding agents when working with code in this repository.
This is the Craft CMS 6.x repository. Craft CMS 6 is a major rewrite that migrates from Yii2 to Laravel 12 while maintaining backwards compatibility through a Yii2 adapter layer.
# Run all tests (uses Pest)
composer tests
# Run a specific test file
./vendor/bin/pest --compact tests/Field/FieldTest.php
# Run a specific test
./vendor/bin/pest --compact --filter="test name"
# Run yii2-adapter Laravel tests
composer tests-adapter
# Run yii2-adapter legacy tests (in the yii2-adapter directory)
./vendor/bin/codecept run unit --filter="test name"- When running tests with
./vendor/bin/pest, always add--compactto reduce output verbosity and token count. - When writing tests, don't use Mockery or Mocks unless absolutely necessary. Prefer using Laravel's Facade fakes or running real code. Tests written are feature or integration tests and not unit tests.
# Fix code style (runs rector, pint, and ecs in parallel)
composer fix-cs
# Run static analysis
composer phpstan
# Run rector only
composer rector# Run Vite dev server for control panel assets
npm run dev
# Build control panel assets for production
npm run build
# Run web components package in watch mode
npm run dev:cp
# Build web components for production
npm run build:cp
# Run tests for @craftcms/cp package
npm run test:cp
# Build all frontend assets (bundles, cp, main)
npm run build:allnpm run build:bundles
npm run dev:bundles -- -- --config-name=cpCraft 6 runs on Laravel 12 but maintains backwards compatibility with legacy Yii2 code through craftcms/yii2-adapter.
Key architectural points:
- Primary namespace:
CraftCms\Cms\insrc/ - Main service provider:
CraftServiceProvider(aggregates ~20 domain service providers) - Legacy Yii2 app bootstrapped via
Yii2ServiceProviderwhich createsCraft::$app - The
yii2-adapter/directory is a Composer path repository (craftcms/yii2-adapter: self.version) - The goal before the next release is to have no Yii code or references in the
srcfolder, theyii2-adapterprovides fallbacks and compatibility layers. - This is a large codebase, with large files. Be mindful of file size and search before you read.
The codebase is organized by domain in src/:
Element/- Base element system (entries, assets, users are all elements)Element/Concerns/- Traits that compose Element functionality (e.g.,HasRoutesAndUrls,Cacheable,HasCanonical)
Entry/- Entry element type and related servicesField/- Field types and field layout systemDatabase/- Migrations, queries, and the customElementQuerysystemProjectConfig/- YAML-based configuration managementPlugin/- Plugin system and loadingUser/- Authentication, permissions, user groupsSite/- Multi-site support
Element queries (src/Database/Queries/) are the primary way to query content:
Entry::find(),Asset::find(),User::find()return specializedElementQueryobjects.ElementQueryimplementsIlluminate\Contracts\Database\Query\Builderand forwards calls to a Laravel query builder.- Use
joinElementTable(Table::ENTRIES)to join specialized element tables and gain access to their columns. - Support eager loading (
with()), caching, and custom field queries. - Located in
src/Database/Queries/with concerns inConcerns/subdirectory. - Important: Eloquent classes which are in a
Modelsnamespace are not usually how content is queries. Use the elements instead if they exist, namespaced inElements.
- Use
CraftCms\Cms\Database\Tableconstants for all table names (e.g.,Table::ENTRIES,Table::ELEMENTS). - Prefer Laravel's schema builder and migrations over raw SQL.
- When working with elements, always respect the
elementsandelements_sitesbase tables.
- Use the
t()helper function for translations:t('Settings'). The second argument is optional and specifies the translation categoryapp(default) orsite. - The
t()helper is a wrapper aroundCraftCms\Cms\Support\Facades\I18N::translate().
- Craft config:
config/craft/(general.php, custom-fields.php, etc.) - Project config:
config/craft/project/(YAML files for schema) - Laravel config:
config/(standard Laravel configuration) - Craft-specific configuration lives in the
GeneralConfigclass, not in generic Laravel config files. When adding new configuration options, add them as properties onGeneralConfig.
- Uses Pest with Orchestra Testbench for Laravel package testing
- Base test class:
CraftCms\Cms\Tests\TestCase - Tests require database - configure via
tests/.env(copy from.env.example.mysqlor.env.example.pgsql) - Uses
RefreshDatabasetrait - tables are migrated fresh via theInstallmigration - Classes marked
finalhave this keyword stripped during testing - you can create custom test classes that extend production classes (e.g., extendingUserelement) to override methods likegetFieldLayout()for easier testing without complex mocks
Important: When creating or adjusting tests, take a look at @docs/TESTING.md for patterns and best practices.
Each domain has its own service provider (e.g., FieldsServiceProvider, UserServiceProvider) registered through the aggregate CraftServiceProvider.
Services that should be singletons use the #[Singleton] attribute from Laravel:
use Illuminate\Container\Attributes\Singleton;
#[Singleton]
final class MyService
{
public function __construct(
private readonly SomeDependency $dependency,
) {}
}The service can then be resolved via app(MyService::class) and will return the same instance each time.
Craft 6 uses Laravel's routing system, coordinated through src/Route/RouteServiceProvider.php.
Route Registration:
routes/routes.phpis the main entry point for route loading.routes/actions.phpcontains action-style routes (e.g.,actions/users/login), registered under thecraft.actions.name prefix.routes/cp.phpcontains Control Panel routes, registered under thecraft.cp.name prefix and prefixed with thecpTriggerconfig value.routes/web.phpcontains frontend web routes, registered under thecraft.name prefix.
Middleware Groups:
craft: Base middleware for all Craft requests (locale, schema checks, etc.)craft.cp: Middleware specific to Control Panel requests (requires CP request, license checks)craft.web: Middleware for frontend web requests
Dynamic Routing:
- Action Requests:
HandleActionRequestmiddleware dynamically routes requests that use theactionparameter or theactions/URI prefix. - Token Requests:
HandleTokenRequestmiddleware handles requests with atokenparameter (e.g., for previews). - Project Config Routes: Dynamic routes defined by users in the Control Panel are loaded from Project Config and registered during the
bootedlifecycle of the application.
Craft 6 uses Laravel's event system. Events are simple classes with public properties located in Events/ subdirectories within each domain (e.g., src/Element/Events/).
Laravel Event Pattern:
// Event class (src/Element/Events/BeforeSave.php)
final class BeforeSave
{
use \CraftCms\Cms\Shared\Concerns\ValidatableEvent; // For cancellable events
use \CraftCms\Cms\Shared\Concerns\HandleableEvent; // For events able to be marked as handled
public function __construct(
public \CraftCms\Cms\Element\Element $element,
public bool $isNew,
) {}
}
// Dispatching the event
event($event = new BeforeSave($this, $isNew));
if (!$event->isValid) {
return false;
}Yii2 Backwards Compatibility:
- Legacy
EVENT_*constants are maintained inyii2-adapter/legacy/base/Element.php - The
registerEvents()method in the yii2-adapter bridges Laravel events to Yii2 handlers - When referencing yii2-adapter constants from
src/, use an alias:use craft\base\Element as YiiElement;
Key directories:
src/Element/Events/- Element lifecycle and UI eventssrc/Entry/Events/- Entry-specific eventssrc/User/Events/- User-related eventssrc/Plugin/Events/- Plugin lifecycle events
A portion of this port is moving from twig + jQuery templates into Inertia + VueJs. The original twig templates are conttained in resources/templates the new Inertia Vue files will live in resources/js.
We're also building component library located in the @craftcms/cp package. Whenever possible, use components from that package to build out UI.
Some source files contain Unicode characters (e.g., curly/smart quotes ' instead of ASCII ') in comments and strings. These cause edit_file string matching to fail silently. If edits fail to match, inspect the file with hex tools to check for non-ASCII characters.
The yii2-adapter/composer.json maps the craft\ namespace to the legacy/ directory. New classes in the adapter must be placed under yii2-adapter/legacy/ to be autoloaded correctly. Placing files in yii2-adapter/lib/ will cause "Class not found" errors.
PHP 8.2+ does not allow accessing constants on traits directly (e.g., MyTrait::SOME_CONSTANT). Access them through a class that uses the trait instead.
- Uses Laravel Pint with Laravel preset
- Rector for automated refactoring (PHP 8.4, Laravel-specific rules)
declare(strict_types=1)required in all PHP files- Final classes by default,
readonlywhen possible - ECS for yii2-adapter code style
- You don't need to remove unused imports, running Pint will fix that for you