craftcms/craft6-revamp
A revamped Craft CMS 6 starter project from the Craft team, offering a modern scaffold for building content-managed PHP sites. It provides updated structure and configuration to help developers bootstrap Craft 6 installations quickly and explore the next generation of Craft workflows.
craftcms/craft6-revamp is not a library you composer require into an existing app — it is a project scaffold, consumed once via composer create-project. Think laravel/laravel, not laravel/framework. After the create step, all the code lives in your repository and the package's job is done. The value is entirely in the conventions it establishes: directory layout, config structure, environment handling, and template/asset organization.
Under the hood this is Yii2, not Laravel, so your first hour is mostly vocabulary translation:
| Laravel | Craft 6 equivalent |
|---|---|
php artisan |
php craft |
.env + config/*.php |
.env + config/*.php + config/project/*.yaml |
| Eloquent models / query builder | Elements + Element Queries |
| Blade | Twig |
| Service providers | Plugins and Modules |
| Migrations | Migrations (nearly identical shape) |
app(Foo::class) |
Craft::$app->getFoo() |
Minimal first run:
composer create-project craftcms/craft6-revamp my-site
cd my-site
cp .env.example .env # set APP_URL, DB_*, SECURITY_KEY
php craft install # or point at an existing DB dump + project config
php craft serve # or your usual Valet / DDEV / Sail-style setup
Read files in this order: .env.example → config/general.php → config/project/ → modules/ → templates/. If you only read one thing, read config/project/. That directory is your database schema, field layouts, sections, and permissions expressed as YAML — it is the single biggest mental shift coming from Laravel, where schema lives in migrations and content models live in code.
First real use case: create a section (content type) in the control panel, export project config, commit the YAML, then have a second developer pull and run php craft up. That round trip — CP change → YAML diff → commit → craft up — is the loop you will live in every day.
Project config is the source of truth. Locally, allowAdminChanges => true so every CP edit writes to config/project/*.yaml; commit those files. In staging/production, allowAdminChanges => false and apply config as a deploy step. Never let content-model changes happen through the production CP — they will silently diverge from YAML and the next deploy will fight you.
Deploy sequence that actually works:
composer install --no-dev --optimize-autoloader
php craft up --interactive=0 # migrations + project config in one shot
php craft clear-caches all
php craft queue/listen --verbose & # or Supervisor, exactly like queue:work
Elements instead of Eloquent. Element queries are lazy and chainable, so they feel familiar — but the N+1 footgun is worse than Eloquent's, because there is no with() that "just works" by default. You must name every relation you intend to touch, and forgetting one is invisible until you profile.
use craft\elements\Entry;
$posts = Entry::find()
->section('blog')
->with(['author', 'heroImage', 'topics']) // non-negotiable
->orderBy(['postDate' => SORT_DESC])
->limit(12)
->all();
Twig is Blade with different filters. {{ }} auto-escapes — do not reach for |raw reflexively the way you reach for {!! !!}. Loops, conditionals, and includes translate almost 1:1; the friction is entirely in filters (|date, |length, |slice, |default, |split) and in reaching for craft.app.* for anything the global craft variable doesn't expose.
Business logic belongs in a Module, not in templates. Generate one with php craft make module; it is a Yii module registered in config/app.php, and it is where controllers, console commands, and event listeners live. This is your app/ directory. Resist installing a plugin for logic that is specific to one site.
// modules/site/Module.php
use craft\elements\Entry;
use craft\events\ModelEvent;
use yii\base\Event;
Event::on(Entry::class, Entry::EVENT_AFTER_SAVE, function (ModelEvent $e) {
// dispatch a queue job, ping a webhook, invalidate a cache key
});
Queue everything slow. Image transforms, external API calls, and mail all go on the queue. php craft queue/listen is your queue:work; wire it into Supervisor identically to how you would a Laravel worker.
Reach for the Content API before hand-rolling endpoints. Before writing a controller, check whether Element API or Craft's built-in GraphQL returns the shape you need. For headless or JS-driven front ends it usually does, and it saves you re-implementing permission and status checks.
Plugins via Composer, not the plugin store. The store is fine for local exploration; composer.json is what makes a deploy reproducible. Enable/disable state lives in project config, so the two must stay in sync or craft up will complain.
compare/1.1.1...1.1.2 with no prose is the signature of a patch release — dependency bumps, template/CSS tweaks, small fixes. Semver says no breaking changes, but the notes disclose zero detail, so treat it as "assume safe, verify before merging." Diff config/ and config/project/ between the tags: a scaffold release that ships altered project config can silently reshape your content model.php craft project-config/apply will either refuse or overwrite depending on flags. --force wins — take a mysqldump first. Symptoms: fields vanish from the CP after a pull, or project-config/diff shows changes nobody admits to making.allowAdminChanges => false also blocks some php craft commands that write config. Expect confusing "changes not allowed" errors on a machine where you forgot to flip it back for local work. Keep it environment-driven (App::env('ENVIRONMENT')), never hardcoded.php craft clear-caches all handles data caches; Twig compile caches live in storage/runtime/compiled-templates and sometimes need a manual purge after a deploy that moves template paths — otherwise you get stale-render ghosts.devMode => true leaks stack traces and disables caching. Verify it is off before reporting a performance problem; a staging box left in dev mode makes every query look ~10× slower than production and sends you chasing the wrong bottleneck.Entry::find() inside a {% for %}, hoist it out or use craft.entries with .with(). This is the one pattern that consistently bites Laravel devs who are used to lazy loading being "good enough."Craft::$app->getFoo() / Craft::$app->set(), not app(Foo::class). If you are porting an internal package that assumes Laravel's container, this is the seam that will fight you hardest.EVENT_REGISTER_SITE_URL_RULES, Elements::EVENT_REGISTER_ELEMENT_TYPES, View::EVENT_REGISTER_TEMPLATE_ROOTS, Users::EVENT_AFTER_LOGIN. These cover most "I need to hook into the framework" needs without forking anything.config/, composer.json, and CHANGELOG.md. If those are untouched, 1.1.2 requires no migration work and no change to your assessment — just bump and move on.How can I help you explore Laravel packages today?