diff --git a/docs/.vuepress/anchor-prefixes.js b/docs/.vuepress/anchor-prefixes.js index c3c356e96..1da0f2168 100644 --- a/docs/.vuepress/anchor-prefixes.js +++ b/docs/.vuepress/anchor-prefixes.js @@ -10,6 +10,7 @@ * - `source` for GitHub-hosted file references. */ module.exports = { + 'craft6': { base: 'https://docs.craftcms.com/api/v6/', format: 'internal' }, 'craft5': { base: 'https://docs.craftcms.com/api/v5/', format: 'internal' }, 'craft4': { base: 'https://docs.craftcms.com/api/v4/', format: 'internal' }, 'craft3': { base: 'https://docs.craftcms.com/api/v3/', format: 'internal' }, @@ -22,6 +23,7 @@ module.exports = { 'yii2': { base: 'https://www.yiiframework.com/doc/api/2.0/', format: 'yii' }, 'yii1': { base: 'https://www.yiiframework.com/doc/api/1.1/', format: 'yii' }, 'guide': { base: 'https://www.yiiframework.com/doc/guide/2.0/en/', format: 'generic' }, + 'laravel': { base: 'https://laravel.com/docs/13.x/', format: 'generic' }, 'config5': { base: '/5.x/reference/config/general.md#', format: 'config' }, 'config4': { base: '/4.x/config/general.md#', format: 'config' }, 'config3': { base: '/3.x/config/config-settings.md#', format: 'config' }, diff --git a/docs/.vuepress/sets/craft-cms.js b/docs/.vuepress/sets/craft-cms.js index 0723435d1..5f458807d 100644 --- a/docs/.vuepress/sets/craft-cms.js +++ b/docs/.vuepress/sets/craft-cms.js @@ -5,6 +5,7 @@ module.exports = { icon: "/docs/icons/craft.svg", baseDir: "", versions: [ + ["6.x", { label: "6.x", stabilityFlag: 'alpha' }], ["5.x", { label: "5.x" }], ["4.x", { label: "4.x" }], ["3.x", { label: "3.x", isEol: true }], @@ -15,6 +16,64 @@ module.exports = { searchPlaceholder: "Search the Craft docs", primarySet: true, sidebar: { + "6.x": { + "/": [ + { + title: "Intro", + collapsable: false, + children: [ + ["", "About the Alpha"], + ], + }, + { + title: "Projects", + collapsable: false, + children: [ + "requirements", + "install", + "upgrade", + ], + }, + { + title: "Extensions", + collapsable: false, + children: [ + "extend/introduction", + "extend/adapter", + // "extend/resources", + "extend/upgrade", + "extend/avenues", + "extend/approach", + "extend/local-dev", + ], + }, + { + title: "Plugin Upgrade Guide", + collapsable: false, + children: [ + "extend/provider", + "extend/assets", + "extend/macros", + "extend/commands", + "extend/config", + "extend/http", + // "extend/elements", + "extend/events", + "extend/fields", + "extend/disks", + "extend/helpers", + "extend/queue", + "extend/logging", + "extend/mail", + "extend/models", + "extend/services", + "extend/session", + "extend/templates", + "extend/validation", + ], + } + ], + }, "5.x": { // Sections here are greedily-matched, so the more specific ones must come first: "/reference/": [ diff --git a/docs/.vuepress/theme/components/PostHeading.vue b/docs/.vuepress/theme/components/PostHeading.vue index 1770678de..488635d0e 100644 --- a/docs/.vuepress/theme/components/PostHeading.vue +++ b/docs/.vuepress/theme/components/PostHeading.vue @@ -14,6 +14,11 @@ class="version-warning"> You are viewing documentation for an unreleased version of {{ $activeSet.setTitle }}. Please be aware that some pages, screenshots, and technical reference may still reflect older versions. +
+ You are viewing documentation for an unreleased version of {{ $activeSet.setTitle }}. Please be aware that the material is changing frequently and may be incomplete or inaccurate, and links may point back to older versions. +
+ +People who use Craft love its… + +- …intuitive, user-friendly [control panel](/5.x/system/control-panel.md) for content creation and administrative tasks; +- …clean-slate approach to content modeling and [front-end development](/5.x/development/README.md) that doesn’t make any assumptions about your content or how it should be delivered; +- …official Plugin Store with hundreds of free and commercial [plugins](https://plugins.craftcms.com/); +- …robust framework for [plugin development](extend/README.md); +- …active, vibrant [community](https://craftcms.com/community); + +You can learn all about it at [craftcms.com](https://craftcms.com). + +## Getting Started + +Welcome to the alpha! +Ready to try out a bleeding-edge version of Craft? +Spin up a [new project](install.md) or [upgrade](upgrade.md) an existing one. + + + +You’ll notice that the docs are relatively sparse for 6.x! +Our work so far as focused on the Laravel port and adapter, but we are slowly pivoting into feature development on the new platform. +Craft remains functionally very similar to 5.x, and most of the control panel will be identical… so we’re using this space to focus on the upgrade process! +Closer to release, we’ll start replicating the structure and content of 5.x. + + + +## Tech Specs + +Craft is a self-hosted PHP application, built on [Laravel](https://www.laravel.com/). +It can connect to MySQL and PostgreSQL databases for content storage. +Server-side rendering is powered by [Twig](https://twig.symfony.com), and headless applications have access to content via a GraphQL API. + + diff --git a/docs/6.x/extend/adapter.md b/docs/6.x/extend/adapter.md new file mode 100644 index 000000000..fcfdfa92c --- /dev/null +++ b/docs/6.x/extend/adapter.md @@ -0,0 +1,117 @@ +# The Adapter + +To reduce the friction for plugin developers (and other early-adopters, in turn), we’ve created a sophisticated compatibility layer that translates legacy API calls to the new Laravel app. + +The first step to getting started with your port or compatibility release is requiring the adapter package: + +```bash +composer require craftcms/yii2-adapter +``` + +Keep it as a dependency of your package as long as _you_ actually need it; don’t assume the project will include it! + +::: tip +You must address _all_ preexisting deprecation warnings (from 5.x) for the adapter to work. +::: + +The adapter package is *not* just a copy of the Craft 5.x application; if you go source-diving, you’ll find that most methods have been reduced to wrappers around the new Laravel application’s APIs. +When possible, we’ve actually extended the _new_ classes and backfilled any methods, properties, and constants that were removed, internally. + +We have also added `@deprecated` tags and `#[Deprecated]` attributes to methods and classes that point to suitable replacements, and deprecation errors when old code paths are reached. + +::: warning +At this point, _most plugins will be fully compatible with Craft 6.x_. + +The rest of this section describes how the adapter works, and how you would go about writing a compatibility layer of your own. +Broadly speaking, this will _not_ be necessary, and you can safely continue from the [required changes](upgrade.md) page. +Consider returning here when your plugin has been fully ported! +::: + +## Legacy Support + +You are not obligated to provide legacy API support—especially if your plugin is not extensible by other plugins (or from individual projects). + +The most common use case for this section is apt to be plugins that store fully-qualified class names in the database or project config. +As an example, a plugin that lets administrators configure notifications and stores their `type` in the database (corresponding directly to a class) should at least move the class into your new namespace and map the old name using PHP’s `class_alias()`. + +While you can solve this in part with a migration that substitutes the old classes, there may be a window of time when the app expects those old classes to exist _before_ the migration can run. + +::: tip +If you _do_ end up with any kind of compatibility layer, `composer.json` must include your new *and* old `autoload.psr-4` namespaces. +::: + +### Your own adapter + +Due to its size, Craft’s adapter lives in an external package. +It also results in all of Yii being pulled in as a dependency, which should *not* be necessary for most plugins: your goal should be to have your entire plugin *and* its own compatibility layer (if one is necessary at all) running natively in the new Laravel environment. + +Your adapter’s primary function is to map existing projects’ lingering references to old classes (and class members) to their new counterparts. +Here’s an idea for the kinds of changes to expect, based on our first-party plugins: + +1. Add `@deprecated` tags and move existing classes into a new `legacy/` auto-loading root. The *namespace* for legacy code should stay the same, but the *root* must be updated in `composer.json`. +2. Add your new namespace to the auto-loading configuration in `composer.json` (say, `MyOrg\\MyPlugin\\`), pointing to `src/`. +3. Create classes in `src/` using the new namespace, and gradually translate implementations. That translation process will involve reviewing the rest of the topics in the migration guide, and may come in a few different forms: + - Delete and alias completely compatible classes to make them available in your old namespace: + + ```php + // We’re in the old namespace! + namespace myorg\pluginname\base; + + // (Optional) Keep original class definition inside conditional so it can still be picked up by IDEs: + if (false) { + /** + * @deprecated 2.0.0 {@see \MyOrg\PluginName\Contracts\SomeInterface} + */ + interface SomeInterface + {} + } + + // Declare alias to new class: + class_alias(\MyOrg\PluginName\Contracts\SomeInterface::class, SomeInterface::class)) + ``` + + - For classes with shallow inheritance (in particular, those whose parents all belong to your plugin or an external dependency other than Craft or Yii), you can instead move the implementation into the new namespace and `extend` it in the old. (See: `craft\auth\methods\RecoveryCodes`) + - For classes that have been split up, restructured, or are otherwise incompatible, you can keep parts or whole legacy classes around. Whenever feasible, log deprecation before calling your new API: + + ```php + namespace myorg\pluginname\services; + + use MyOrg\PluginName\Shipping\Actions\ConfirmSignature; + use MyOrg\PluginName\Models\Delivery; + + class Shipping + { + public function confirmSignature(Delivery $delivery, Asset $proof): void + { + \CraftCms\Cms\Support\Facades\Deprecator::log(__METHOD__, 'myorg\pluginname\services\Shipping::confirmSignature() is deprecated. Dispatch the MyOrg\PluginName\Shipping\Actions\ConfirmSignature action, instead.'); + + // Supposing we implemented this as a job or event + ConfirmSignature::dispatch($delivery, $proof); + } + } + ``` + +Here’s an example of a plugin class that had previously mounted “services” an exposed them via getters: + +```php +namespace myorg\myplugin; + +use CraftCms\Cms\Support\Facades\Deprecator; + +class Analytics extends \MyOrg\MyPlugin\Analytics +{ + // ... + + public function getReports(): Reports + { + Deprecator::log(__METHOD__, 'myorg\myplugin\Analytics::getReports() is deprecated. Get an instance of MyOrg\MyPlugin\Analytics\Reports\Manager using the Laravel service container, instead.'); + + return app(MyOrg\MyPlugin\Analytics\Reports\Manager::class); + } +} +``` + +The legacy events system belonged almost entirely to Yii, so you will not be able to proxy events without the adapter. +If this continuity is important to you and downstream developers, you can either require the adapter or provide clear upgrade instructions to reduce their workload. + +See `craft\elements\User` for an example of a class that forwards new events to Yii. We elected to group classes’ proxy handlers in a static `registerEvents()` method, which we call from a central location: `CraftCms\Yii2Adapter\Event\EventCompatibility::boot()` diff --git a/docs/6.x/extend/approach.md b/docs/6.x/extend/approach.md new file mode 100644 index 000000000..247743a4e --- /dev/null +++ b/docs/6.x/extend/approach.md @@ -0,0 +1,19 @@ +# Approach + +Simple plugins (like those that exist mostly to handle or alter an event) can probably be **fully ported** and released as an alpha. + +We recommend that developers of larger plugins (which provide entirely new features or new types of existing components) make the minimum required updates for compatibility, then use the alpha to gradually migrate. This will unblock others who wish to test the upgrade process in real projects and report issues with Craft, the compatibility layer, or your plugin. + +::: tip +If you maintain a plugin that provides its own extension surface (events, service APIs, etc.), you should decide and telegraph whether you intend to fully port your plugin during the alpha, and whether you intend to ship a compatibility layer. +::: + +## Necessity + +Some low-level plugins may not be strictly necessary in the Laravel ecosystem, but developers will still appreciate a configuration layer that is accessible via the control panel and tracked in project config. + +Examples of this are: + +- **Mail transport adapters** — Projects can set up mailers via `config/mail.php` and select one in +- **Custom log back-ends** — As with prior versions of Craft, plugins may not be initialized early enough in the app’s lifecycle to capture a complete picture. Consider whether projects can effectively use Laravel’s built-in [logging](laravellogging) tools. +- **Filesystem types** — Developers can directly configure *disks* and use them for volumes. The screen also acts as a disk configurator. diff --git a/docs/6.x/extend/assets.md b/docs/6.x/extend/assets.md new file mode 100644 index 000000000..e8d807402 --- /dev/null +++ b/docs/6.x/extend/assets.md @@ -0,0 +1,165 @@ +# Asset and Publishables + +## Asset Bundles + +Craft includes a lightweight implementation of Yii’s asset bundle system, to help with the transition. +This is an *alternative* to legacy asset registration, and still requires some changes to work. + +::: tip +If you had previously maintained an asset bundle to register a single resource (with no dependencies) in the control panel, consider using your plugin’s `$scripts` or `$styles` property. +::: + +Your asset bundle(s) must adopt a new interface and define a `register()` method: + +```php +namespace MyOrg\MyPlugin\Assets; + +use MyOrg\MyPlugin\Plugin; +use CraftCms\Cms\View\LegacyAssets\LegacyAssetInterface; + +class ActivityGraph implements LegacyAssetInterface +{ + // Optional: + public array $depends = [ + \CraftCms\Cms\View\LegacyAssets\D3Asset::class, + ]; + + // Assets defined `$css` and `$js` should be directly registered against the passed HtmlStack: + public function register(HtmlStack $htmlStack): void + { + $plugin = Plugin::getInstance(); + $publicPath = public_path("vendor/{$plugin->packageName}"); + + $htmlStack->jsFile($publicPath.'/build/js/activity.js')); + $htmlStack->cssFile($publicPath.'/build/css/activity.css')); + } +} +``` + +Then, whenever you’re preparing a response that will use the asset (typically in a controller), let the registry know: + +```php +use MyOrg\MyPlugin\Assets\ActivityGraph; +use CraftCms\Cms\View\LegacyAssets\InternalAssetRegistry; + +app(InternalAssetRegistry::class)->register(ActivityGraph::class); +``` + +When the app is ready to inject your asset into a response, the _asset’s_ `register()` method is called. + +::: warning +Note that the `InternalAssetRegistry` class (and all our own asset bundles) have been deprecated, proactively. +This is a stopgap solution that allowed us to fully eject the adapter for new projects, while gradually rebuilding the control on [Inertia](https://inertiajs.com). +::: + +## Asset Pipelines + +You are free to handle assets in whatever way fits your needs. + +Plugins can register scripts and styles for every control panel request by declaring a map of paths as `$scripts` and `$styles`. +These assets (the keys) become *publishable*, and will be copied into `public/vendor/myorg/plugin-name/...` (the value) when your plugin is installed. +Craft also takes care of deleting them from that directory when your plugin is uninstalled. + +```php +class Activity extends Plugin +{ + // ... + + public array $scripts = [ + __DIR__.'/../resources/css/sparkline.css' => 'css/sparkline.css', + ]; + + public array $styles = + __DIR__.'/../resources/js/sparkline.js' => 'js/sparkline.js', + ]; + + // ... +} +``` + +::: tip +The paths you declare in your plugin’s `$scripts` and `$styles` properties should be the final, “built” assets, not their source files. +Whenever you make changes to the files in `resources/`, you will need to re-publish them: + +```bash +ddev artisan vendor:publish --tag=your-plugin-handle +``` +::: + +Any additional publishable files can be declared in the same way as scripts or styles, using the `$publishables` property. +See `CraftCms\Cms\Plugin\Concerns\PublishesFiles` for more information about how this is handled. + +### Vite + +If you don’t have a preexisting bundler, consider using Laravel’s built-in [Vite pipeline](laravel:vite). +Add a `$vite` property to your plugin with any number of entry points (or `inputs`) to register them as publishable files *and* hook into Laravel’s automatic `hot` file discovery: + +```php +class MyPlugin extends \CraftCms\Cms\Plugin\Plugin +{ + protected array $vite = [ + 'input' => [ + 'resources/js/plugin.js', + 'resources/css/plugin.css', + ], + 'publicDirectory' => 'resources/dist', + 'buildDirectory' => 'build', + ]; +} +``` + +Despite the name, `publicDirectory` is not a path within Laravel’s `public/` directory! +It just defines the root folder the Vite dev server exposes, and is relative to your package’s root directory. +`buildDirectory` is appended to the `publicDirectory` to register and serve built assets when Vite is *not* running—Craft registers the equivalent `style` and `script` tags, based on the manifest output by Vite during a production build. + +Your minimum configuration in `vite.config.js` should look something like this, matching the input and output paths in the plugin: + +```jsx +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/js/plugin.js', + 'resources/css/plugin.css' + ], + publicDirectory: 'resources/dist', + }), + ], +}); +``` + +This scaffolding is present in our 6.x-ready [demo plugin](repo:craftcms/demo-plugin). +Additional config may be required for compatibility with your development environment; DDEV, for example, benefits from special CORS rules and customization of the dev server: + +```jsx +// ... + +const host = '0.0.0.0'; +const port = 3000; +const origin = `${process.env.DDEV_PRIMARY_URL}:${port}`; + +export default defineConfig({ + server: { + host, + origin, + port, + cors: { + // Live dangerously... + origin: true, + // ...or permit only DDEV origins. + // See: https://dev.to/mandrasch/vite-is-suddenly-not-working-anymore-due-to-cors-error-ddev-3673 + // origin: /https?:\/\/([A-Za-z0-9\-\.]+)?(\.ddev\.site)(?::\d+)?$/, + }, + }, + + // ... +}); +``` + +If you want to work on multiple plugins at once, each will need a unique `port` and `hotFile`. +The Vite server config block (JS) needs both values; your plugin (PHP) only needs the `hotFile`. + +See `CraftCms\Cms\Plugin\Concerns\HasFrontendAssets` for more information about the `$vite` configuration. diff --git a/docs/6.x/extend/avenues.md b/docs/6.x/extend/avenues.md new file mode 100644 index 000000000..01b3bf376 --- /dev/null +++ b/docs/6.x/extend/avenues.md @@ -0,0 +1,160 @@ +# Extension Types + +Historically, you’ve been able to extend Craft in two ways: [plugins](#plugins) (distributable packages installed via the [Plugin Store](https://plugins.craftcms.com)) and [modules](#modules) (a low-level extension usually tied to a single project, but still somewhat portable). + +## Plugins + +Plugins are special `craft-plugin`-typed Composer packages that have additional hooks for discovery by Laravel and Craft. +You must install plugins with Composer for them to be discovered by our `craftcms/plugin-installer` package. + +You can clone a plugin into your project directory and require it with a path repository to [work locally](local-dev.md). + +## Modules + +Project-specific code should be unfurled into the `App` namespace. +“Modules” are not really a distinct concept any more; if you want to share or distribute code/features for use in other projects, check out Laravel’s documentation on [package development](laravel:packages). + +The equivalent entry point for modules is a regular [service provider](laravel:providers). +Your application can have as many service providers as you need. +Add new providers to `bootstrap/providers.php`, or turn your primary provider into an `\Illuminate\Support\AggregateServiceProvider`. + +## New Concepts + +This section discusses some general design differences between Yii and Laravel that will affect how you update existing plugins and structure new ones. + +### Feature Awareness + +Laravel is designed to accommodate a wide range of private apps, so packages can’t assume that all projects are configured the same. +Like Yii, some features, drivers, database tables, etc. are elective and may not be available. +Craft may even be invited into someone’s sprawling, preexisting application with extremely specific infrastructure requirements! + +Confirm with your users when you rely on non-standard Laravel features (like [batched queue jobs](laravel:queues#job-batching)), and avoid using them while the app is bootstrapping, or during installation. + +### PHP Support + +Craft now requires PHP 8.5, so every plugin gets access to language features introduced since 8.2: + +- A proper [URI](https://www.php.net/releases/8.5/en.php#new-uri-extension) built-in +- [Pipe operator](https://www.php.net/releases/8.5/en.php#pipe-operator) +- [Property hooks](https://www.php.net/releases/8.4/en.php#property_hooks) (getters and setters) +- [New `array_*` functions](https://www.php.net/releases/8.4/en.php#new_array_find) +- [Typed class constants](https://www.php.net/releases/8.3/en.php#typed_class_constants) +- [`#[Override]` attribute](https://www.php.net/releases/8.3/en.php#override_attribute) (protect against drift from interfaces/subclasses) + +Consider reviewing your code for opportunities to implement these features. +In modernizing the Craft codebase, we also made extensive use of **enumerations** and **attributes**. +Many sections in this guide provide opportunities to annotate your code with attributes, rather than implement methods. + +### Organization + +Craft’s classes have been reorganized by feature rather than parentage, and most have a significantly shallower inheritance tree. +You are free to adopt this pattern or choose your own structure based on needs in the new system. + +For example, everything related to communication with a third-party API could be in a `Bridge\` namespace (`enum` classes, exceptions, events, etc.) while user-facing data collection is in a `Customer\` namespace. + +Autoloading still observes the same rules as before: + +- The fully-qualified class name/space must match its path on disk, within the autoloading root (defined in `composer.json`). +- Treat all files and paths as **case-sensitive** to support case-sensitive filesystems. + +#### Paths + +Check out the various plugin traits for any hard-and-fast rules about where classes and [resources](assets.md) live. +The most common of these is apt to be your [routes](http.md) files (`HasRoutes`). + +### Models + Data Objects + +Laravel’s definition of a [model](models.md) is much narrower than Craft’s, and combines aspects of our legacy *model* and *record* classes. + +1. Anything that you previously used `craft\db\Record` for in Craft 5.x will likely become an Eloquent model. +1. We’ve brought some of our legacy “model” features into 6.x as *components*. Components are still a perfectly good way to move “data objects” throughout your plugin. +1. [Validation](validation.md) in Laravel is more often performed on simple arrays at the input or request layer rather than after assignment to models. + +We discuss this in greater detail in the [Models, Records, and Data](models.md) section. + +### Initialization + +Plugins have an entirely new, phased initialization mechanism that makes it much easier to reason about the availability of other systems and add features in a declarative way. + +As the application boots, Laravel instantiates each known service provider, including Craft and all plugins. +Those providers are then asked to [register](laravel:providers#the-register-method) any low-level behavior (like how classes are resolved through the container), and are later [booted](laravel:providers#the-boot-method). + +::: tip +You’ll almost never need to implement the `register()` or `registerPlugin()` methods, and the kinds of things you can do in either are extremely limited. + +As an example: it comes so early in the request lifecycle that the events system may not work reliably. +The app is still waiting to be told what class is responsible for dispatching events… and another service provider may re-bind it before an event triggers! +::: + +A simplified view of [the initialization process](laravel:lifecycle) looks like this: + +- Application instance (HTTP or Console kernel) creation +- Service provider discovery +- Service provider registration +- Service provider booting +- Routing + +Only bits and pieces of “Craft” are resolved in this process—unlike Yii, there is no central *Craft* instance that exists prior to plugins being booted! +Despite this, we are able to make sure during the boot phase that only plugins that are installed and enabled are actually “booted.” + +To be notified when the app has fully booted, register a callback: + +```php +app()->booted($callback); +``` + +#### Plugin Traits + +A handful of built-in *concerns* handle this bootstrapping process for plugins, providing a declarative way to register many common for define any of their corresponding properties and methods. +Instead of setting up an event listener to, say, register a field type, + +- `HasCommands` → `$commands` — List of [command](commands.md) classes exposed to the Artisan CLI. +- `HasConfig` → `$config` (Boolean) — Set to `false` to prevent [config](config.md) file publishing and discovery. +- `HasEditions` → `$editions`, `$minCmsEdition`, `static editions()` — Call `is()` to test the current edition and determine limits or gate features. +- `HasElementTypes` → `$elementTypes` — An array of custom [element types](elements.md). +- `HasFieldtypes` → `$fieldTypes` (Array) — Custom [field types](fields.md) class names. +- `HasFrontendAssets` → `$vite`, `$styles`, `$scripts` — Configuration or path maps for various [asset publishing](assets.md) options. +- `HasListeners` → `$events` — Bind [events](events.md) (keys) to listeners (values). +- `HasPermissions` → `getPermissions()` (Array) — `CraftCms\Cms\User\Data\Permission` objects, with optional nesting. +- `HasRoutes` → Loads [routes](http.md) defined in `routes/[web|cp|~~actions~~].php`. +- `HasSettings` → `$hasCpSettings`, `$hasReadOnlyCpSettings` (Boolean) — Enables automatic routing to a dedicated [settings](config.md) screen in the control panel. +- `HasTranslations` → `$t9nCategory`, `$sourceLanguage` — Customize the translation category. Defaults to your plugin handle. +- `HasUtilities` → `$utilities` (Array) — Register utilities. +- `HasViews` — Automatically registers your plugin’s [template](templates.md) root. +- `HasWidgets` → `$widgets` (Array) — Register widgets for the control panel dashboard. + +::: tip +The base plugin class includes all of these traits, by default—you don’t need to opt-in or define properties your plugin doesn’t use. +::: + +As an example, if your plugin provided a new type of dashboard widget, you would add a `$widgets` property to your main plugin class: + +```php{8-10} +use CraftCms\Cms\Plugin\Plugin; +use MyOrg\MyPlugin\Widgets\RandomQuoteWidget; + +class InspirationPlugin extends Plugin +{ + // ... + + protected array $widgets = [ + RandomQuoteWidget::class, + ]; +} +``` + +A plugin’s traits are all initialized before finally calling the `bootPlugin()` method. +Once all plugins are booted, Craft emits the `CraftCms\Cms\Plugin\Events\PluginsLoaded` event, indicating that it’s safe to check for and interact with other plugins. + +::: danger +**Do not** override your plugin’s `register()` or `boot()` methods. +All your initialization logic should be in the `bootPlugin()` methods. +::: + +## Termination + +Your plugin can register cleanup tasks at the end of the app’s lifecycle—roughly equivalent to Yii’s `EVENT_AFTER_REQUEST` event: + +```php +app()->terminating($callback); +``` diff --git a/docs/6.x/extend/commands.md b/docs/6.x/extend/commands.md new file mode 100644 index 000000000..97d27eb9f --- /dev/null +++ b/docs/6.x/extend/commands.md @@ -0,0 +1,77 @@ +# Commands + +Console controllers are now just [commands](laravel:artisan). +API changes aside, the main difference you’ll notice is that there is no inherent organization to commands as there was in Yii: each command is its own class (or closure), and are only grouped by their signature. + +For example, the built-in `project-config:apply` and `project-config:diff` commands are separate classes, sharing the `project-config:` prefix. +You are free to structure your commands however makes sense, but please avoid generic names like `run` or `send`, and consider using a prefix like `my-plugin:` so users can locate related functionality that your plugin provides. + +::: warning +`command` is no longer a valid argument name. +::: + +There are two primary ways to make commands: [classes](#command-class) and [closures](#command-closure). + +## Command Class + +Commands should extend `Illuminate\Console\Command`. + +```php +namespace MyOrg\MyPlugin\Reporting\Commands; + +use MyOrg\MyPlugin\Reporting\Manager; +use CraftCms\Cms\Console\CraftCommand; +use Illuminate\Console\Attributes\Description; +use Illuminate\Console\Attributes\Signature; +use Illuminate\Console\Command; + +#[Signature('report:generate {templateId}')] +#[Description('Start building a report')] +class GenerateReport extends Command +{ + use CraftCommand; + + // Alternatively, the signature can be set via a property: + // protected $signature = 'report:generate {teamId}'; + + public function handle(Manager $manager, int $templateId): void + { + $template = $manager->getTemplateById($templateId); + + $this->info("Queueing report: {$template->name}!"); + + // ... + } +} +``` + +Your command’s [arguments and options](artisan#defining-input-expectations) are determined by its _signature_. +Laravel takes care of matching command arguments with those of the `handle()` method; other arguments are resolved with dependency injection. +In this example, `$manager` isn’t an argument that we’d expect a user to supply—it’s just a [service](services.md) we want access to in that scope. + +Register command classes with your plugin’s `$commands` property: + +```php +public array $commands = [ + MyOrg\MyPlugin\Reporting\Commands\GenerateReport::class, +]; +``` + +## Command Closure + +You can also create simple commands directly in your plugin’s `bootPlugin()` method, using a [laravel:artisan#closure-commands]: + +```php +Artisan::command('report:generate {templateId}', function (Manager $manager, int $templateId) { + $template = $manager->getTemplateById($templateId); + + $this->info("Queueing report: {$template->name}!"); + + // ... +}); +``` + +## Formatting + Output + +Laravel’s command class has a number of [output helper methods](laravel:artisan#writing-output). +Styling is handled by Symfony’s [XML-like tags](https://symfony.com/doc/2.x/console/coloring.html). diff --git a/docs/6.x/extend/config.md b/docs/6.x/extend/config.md new file mode 100644 index 000000000..0c2a38bb5 --- /dev/null +++ b/docs/6.x/extend/config.md @@ -0,0 +1,51 @@ +# Settings + Config + +As mentioned in the [base plugin class](provider.md) section, the return type for your `createSettingsModel()` function has changed. + +## Config Files + +If you want a user-space config file, add a `config/my-plugin-handle.php` to the root of your package and it will be published by the `CraftCms\Cms\Plugin\Concerns\HasConfig` plugin trait, automatically. +Config loaded in this way is overlaid on the object returned by `createSettingsModel()`. + +## Environmental Config + +To support environment variable overrides on your settings class (or any model, for that matter), call `Env::config($class, 'MY_PLUGIN_NS_')`. + +Plugin settings that support environment variables or aliases should wrap their validation rules in the special `CraftCms\Cms\Validation\Rules\EnvValueRule` class to check the *parsed* values, without replacing those properties on the actual model. +You do not need to juggle the submitted values before they’re sent to project config. + +## Initialization + +Yii’s system was heavily configuration-driven: objects were initialized with a config map that would cascade down through any nested objects. +Places you’d use `Craft::createObject()` can be mostly replaced with `app()->make($class, [...])`. + +## Validation + +The new settings model will be compatible for most plugins, but you will need to adopt new [validation](validation.md) patterns to drop the adapter: + +```php +// Remove: +public function defineRules(): array +{} + +// Add... +public function getRules(): array +{ + return array_merge(parent::getRules(), [ + 'adminNotificationAddress' => ['required', 'email:rfc'], + ]); +} + +// ...or: +public function rulesClass(): string +{ + return MyPluginSettingsRuleset::class; +} +``` + +You may also tag the settings model with the `#[Ruleset(MyPluginSettingsRuleset::class)]` attribute. +For more control over how your settings data is normalized, return your own form request class from `getSettingsRequestClass()`. + +::: tip +See our [Guest Entries](repo:craftcms/guest-entries/tree/5.x) plugin for an example of these new techniques. +::: diff --git a/docs/6.x/extend/disks.md b/docs/6.x/extend/disks.md new file mode 100644 index 000000000..116f9f15d --- /dev/null +++ b/docs/6.x/extend/disks.md @@ -0,0 +1,55 @@ +# Filesystems + Disks + +Filesystems in Craft 6.x are a wrapper around Laravel’s [disks](laravel:filesystem) system. +Their primary responsibility is to translate project-config-compatible settings into a valid disk configuration. +Developers can define disks directly via `config/filesystems.php` using any of the natively-supported drivers, or install a filesystem plugin and configure them via the control panel. + +Ultimately, asset volumes’ `fsHandle`s may point to a Craft-defined filesystem, or directly to a disk (identified by a `disk:` prefix). +All asset manipulation is handled through a consistent Flysystem interface. + +Your filesystem class should now extend `CraftCms\Cms\Filesystem\Filesystems\Filesystem` and implement a `getDiskConfig()` method: + +```php +use CraftCms\Cms\Filesystem\Filesystems\Filesystem; +use CraftCms\Cms\Support\Env; + +class Backblaze extends Filesystem +{ + // ... + + public function getDiskConfig(): array + { + return [ + 'driver' => 'b2', + 'bucketId' => Env::parse($this->bucketId), + 'bucketName' => Env::parse($this->bucketName), + 'accountId' => Env::parse($this->accountId), + 'applicationKey' => Env::parse($this->applicationKey), + 'url' => Env::parse($this->url), + // ... + ] + } +} +``` + +Craft takes care of populating an instance of your filesystem class with incoming settings from the edit screen in the control panel. + +::: tip +If your filesystem relies on a non-standard Flysystem adapter, you may need to add it to your `composer.json`. In our example, that would be `gliterd/laravel-backblaze-b2`. +::: + +To register a filesystem type, listen for the `\CraftCms\Cms\Filesystem\Events\RegisterFilesystemTypes` event: + +```php +Event::listen(function (RegisterFilesystemTypes $event) { + $event->types->push(Backblaze::class); +}); +``` + +::: tip +If you would like to look at a complete Craft 6.x-ready example, check out our [AWS S3](https://github.com/craftcms/aws-s3/tree/3.x) plugin. +::: + +## Subpaths + +Return a `prefix` config key from `getDiskConfig()` to create a [scoped](laravel:filesystem#scoped-and-read-only-filesystems) disk that quarantines operations to a non-root directory. diff --git a/docs/6.x/extend/elements.md b/docs/6.x/extend/elements.md new file mode 100644 index 000000000..d5a8cbfb6 --- /dev/null +++ b/docs/6.x/extend/elements.md @@ -0,0 +1 @@ +# Elements diff --git a/docs/6.x/extend/events.md b/docs/6.x/extend/events.md new file mode 100644 index 000000000..c7c0f7e32 --- /dev/null +++ b/docs/6.x/extend/events.md @@ -0,0 +1,113 @@ +# Events + Listeners + +Every event that Craft emits is now a distinct class. +Most “named” event constants (i.e. `EVENT_AFTER_SAVE`) have been translated to a corresponding class. + +::: tip +Due to the increased specificity of event classes, instance-level events and listeners are rare in the new architecture. +::: + +## Events + +An event class does not need to extend from any other class. +Its name should adequately describe what is about to happen (or what has just happened); if there is relevant data, it can be attached using a property. + +```php +namespace MyOrg\Store\Shipping\Events; + +class DeliveryConfirmed {} +``` + +Emit an event using the `event()` helper function: + +```php +event($confirmation = new DeliveryConfirmed); +``` + +If you care about the outcome of the event, store the event instance in a variable, as we’ve done above. + +- **Cancelable** events should `use CraftCms\Cms\Shared\Concerns\ValidatableEvent`, and the emitting context can proceed based on `$event->isValid`. As with the legacy `CancelableEvent`, the last handler to set `isValid` wins. +- **Handleable** events should `use CraftCms\Cms\Shared\Concerns\HandleableEvent`, and the emitting context can check `$event->handled` to determine whether a handler intervened. + +As an alternative to handleable events, `Event::until()` or `event(..., halt: true)` can be used to capture the first non-null return value from a handler. +Similarly, instruct other developers to return `false` from an event to prevent further handlers from running. + +[Dispatchable](laravel:events#dispatching-events) events provide an alternative pattern: + +```php +DeliveryConfirmed::dispatch($args); +``` + +## Listeners + +[Listeners](laravel:events#defining-listeners) can be classes, as well—or any kind of callable! + +The main difference between Yii and Laravel event handlers is that you no longer need to know *where* an event comes from—you just listen for that event. +Specific objects/instances may be attached to an event, but there is no *de facto* `sender` property. + +Bind listeners to events using your plugin’s `$events` property (see the `HasListeners` trait): + +```php +use CraftCms\Cms\Cp\Events\DefineElementCardHtml; +use CraftCms\Cms\Element\Events\DraftApplied; +use CraftCms\Cms\Plugin\Plugin; +use Illuminate\Mail\Events\MessageSending; + +class DemoPlugin extends Plugin +{ + // ... + + // Each item in this array should have an event class as its key, and one or more listeners as values: + protected array $events = [ + // Craft event: + DraftApplied::class => Listeners\LogElementActivity::class, + // Laravel event: + MessageSending::class => Listeners\LogMailMessage::class, + // Multiple listeners can be attached to a single event name: + DefineElementCardHtml::class => [ + // Listener class: + Listeners\AddCardDetails::class, + // “Callable” array: + [self::class, 'redactSensitiveCardData'], + ], + ]; + + // ... +} +``` + +These bindings are explicit, but Laravel can also infer which event a handler is for just from its signature. +Here’s an example of a listener defined as a closure that takes advantage of this: + +```php +use CraftCms\Cms\Filesystem\Events\RegisterFilesystemTypes; +use MyVendor\MyPlugin\Filesystems\Dropbox + +// ... + +public function bootPlugin(): void +{ + // Closures must be bound in your plugin’s bootPlugin() method, as PHP doesn’t support them as static class properties! + Event::listen(function(RegisterFilesystemTypes $event): void { + $event->types->push(Dropbox::class) + }); +} +``` + +Within a *project*, add listener classes to `App\Listeners` and Laravel will [discover](laravel:events#event-discovery) them, automatically—or you can bind them explicitly in your service provider’s `boot()` method. + +Listeners generally shouldn’t return anything, unless the event was emitted with `until()`. +Craft has historically used event properties to register or collect input from multiple handlers (i.e. `$event->types` in the example above), but Laravel also tracks return values from registered handlers and gives some values special significance. + +::: tip +As you update your handlers, enforce this by giving the return value a `void` type. Return `false` from a handler only when you want to stop the event from propagating to other handlers. +::: + +### Subscribers + +[Subscribers](laravel:events#writing-event-subscribers) bundle multiple handlers into a class. +The organization is entirely up to you, but you must register your plugin’s subscribers in `bootPlugin()`: + +```php +Event::subscribe(CustomerRetentionSubscriber::class); +``` diff --git a/docs/6.x/extend/fields.md b/docs/6.x/extend/fields.md new file mode 100644 index 000000000..bee33a74d --- /dev/null +++ b/docs/6.x/extend/fields.md @@ -0,0 +1,24 @@ +# Field Types + +Fields are wholly a Craft concept, and therefore isolated from many of the internal changes. +The most significant differences are apt to be in how you register field types and field layout elements, and those classes’ validation rules. + +## Registration + +Add your field type classes to `Plugin::$fieldTypes` and they will be registered automatically. +Outside a plugin, listen to `CraftCms\Cms\Field\Events\RegisterFieldTypes`, and push your field class into the event’s `types` property. + +Field layout elements must be registered via the `CraftCms\Cms\FieldLayout\Events\DefineUIElements` or `DefineNativeFields` events, as there is no corresponding plugin property. + +### Rules + +Depending on complexity, field types can define two sets of validation rules: + +- `getRules()` — The field’s own settings, set by a developer and stored in project config. +- `getElementRules()` — Rules for the content stored by each instance of the field on an element, which get merged into the elements’ rules at runtime. + +::: tip +The adapter takes care of wrapping Yii validation rules from the corresponding legacy `defineRules()` and `getElementValidationRules()` methods. +::: + + diff --git a/docs/6.x/extend/helpers.md b/docs/6.x/extend/helpers.md new file mode 100644 index 000000000..123dd08d8 --- /dev/null +++ b/docs/6.x/extend/helpers.md @@ -0,0 +1,8 @@ +# Helpers + +Most of Craft’s static helper classes have moved to the `CraftCms\Cms\Support` namespace. + +## Facades + +Laravel’s [Facades](laravel:facades#main-content) architecture is a handy way to define your plugin’s public API. +It also brings [dependency injection](laravel:facades#facades-vs-dependency-injection) support to a helper-like interface. diff --git a/docs/6.x/extend/http.md b/docs/6.x/extend/http.md new file mode 100644 index 000000000..f83cbb6b9 --- /dev/null +++ b/docs/6.x/extend/http.md @@ -0,0 +1,143 @@ +# Controllers + Routing + +Your plugin’s HTTP API is entirely up to you. +Controllers are not automatically discovered or given routes in Craft 6.x, so you are responsible for mapping them using Laravel’s `Route` facade. + +As your plugin is booted, the `CraftCms\Cms\Plugin\Concerns\HasRoutes` concern looks for three files in your plugin’s top-level `routes/` directory, each with a distinct behavior: + +- `web.php` — Front-end routes, with standard middleware (`craft` and `craft.web`). +- `cp.php` — Control panel routes, with additional middleware (`craft.cp`) that automatically enforces basic permissions. These routes are prefixed with your `cpTrigger`. +- `actions.php` — A compatibility layer for Yii’s “action path” routing scheme. These routes are registered twice: once prefixed with your `actionTrigger` using standard middleware, and once prefixed with `{cpTrigger}/{actionTrigger}` using the additional control panel middleware. + +Here’s an example of a `web.php` file: + +```php +use MyOrg\Activity\Http\Controllers\TrackEvent; +use Illuminate\Support\Facades\Route; + +Route::post('activity/track', TrackEvent::class); +``` + +You are free to use these spaces however you see fit! +Keep in mind that each of these files is included within another routing group and the routes you define are subject to some default rules. +See the [middleware](#middleware) section for more info. + +::: tip +Because your controllers are just classes, any app with your plugin installed can create custom routes pointing to them. +You can use this to your advantage by omitting front-end routes altogether, and letting the developer choose where to mount each endpoint. + +An example might be a portal to manage newsletter subscriptions—instead of baking in the plugin’s name to the management interface (i.e. `/super-forms/signup`), you could require developers to define a route to its `ManageSubscriptions` controller. +This is similar to how Craft has required each project to choose where its GraphQL API lives. +::: + +Yii’s automatic routing scheme for controller actions used specific paths that involved the plugin or module’s identifier and kebab-cased controller and action names. +Craft 6.x does not impose any of these requirements—but continuing to prefix routes with your plugin handle is a great way to avoid collisions. + +## Action Paths + +Plugins can define a special `routes/actions.php` file to maintain compatibility with any front-end forms or other external integrations that they don’t control: + +```php +# routes/actions.php + +Route::prefix('activity', function() { + Route::post('events/log', TrackEvent::class); +}); + +# Two routes: +# -> actions/activity/events/log +# -> admin/actions/activity/events/log +``` + +See the [Guest Entries](repo:craftcms/guest-entries/tree/5.x) plugin for an example. + +### Middleware + +All of Craft’s middleware is registered by `CraftCms\Cms\Route\RouteServiceProvider`. +Each middleware group referenced in `HasRoutes` corresponds to a list of handlers in the route service provider. +You can view the final route layout via the `routes:list` command: + +```bash +# -v displays middleware groups for each route. +# -vv lists *every middleware class* that will apply to each route. +ddev artisan route:list --path=some-path-segment -v +``` + +You can create your own middleware and attach it to your routes like any other Laravel application. +It’s `handle()` method will be called with the `Request` object and a `$next` closure. +Middleware operates on [requests *and* responses](laravel:middleware#middleware-and-responses), based on when you yield to the rest of the app by calling `$next($request)`. + +Laravel’s “terminable” middleware is not compatible with all hosting environments, so we recommend using the generalized termination callback to perform cleanup after a response is sent: + +```php +app()->terminating($this->flushLazyEvents()); +``` + +### Authorization + +Controllers in Laravel don’t often do their own authorization, because they’re no longer automatically exposed to the router. +See the [permissions](#) for info about guards, policies, and other middleware that you can define alongside your routes. + +Any route can be guarded with a known permission using the `can` middleware: + +```php +Route::get('events/history', ListHistory::class) + ->middleware(['can:eventsPlugin-viewEventHistory']); +``` + +The [session](session.md) section has information about interacting with the current user. + +### Controllers + +As a result of so much logic being composed from existing middleware, there is no base controller to extend! +You are welcome to continue grouping multiple actions into a single controller class, or split controllers into individual *invokable* classes. + +For convenience, we’ve collected a handful of methods in a `CraftCms\Cms\Http\RespondsWithFlash` trait (like the familiar `asSuccess()` and `asFailure()` methods and their JSON and “model” counterparts). + +Add “dependencies” to a controller’s constructor as properties (for controllers with multiple actions), or to the `__invoke()` or `handle()` methods (for single-action controllers): + +```php +readonly class TrackEvent +{ + public function __construct( + public \Illuminate\Http\Request $request + ) {} + + public function handle( + MyOrg\Activity\Ledger\Events $events + ) { + $event = $this->request->validate([ + 'name' => ['required', 'string'], + ]); + + abort_unless($events->log($event['name'], $request->getUserIp())); + + // ... + } +} +``` + +At first blush, this implementation *looks* wildly permissive and prone to abuse. +In reality, it can be exposed in a completely safe way with the appropriate middleware. +Your plugin can even make these hardening measures configurable: + +```php +public function bootPlugin() +{ + RateLimiter::for('activity', function (Request $request) { + $limit = $this->getSettings()->maxEventsPerMinute; + + return Limit::perMinute($limit)->by($request->getClientIp()); + }); +} +``` + +You’d apply this custom rate-limiting middleware in `routes/web.php`: + +```php +Route::middleware(['throttle:activity'])->group(function () { + Route::post('activity/track', TrackEvent::class); +}); +``` + +A complete example of this kind of configurable routing can be found in our [Guest Entries](https://github.com/craftcms/guest-entries/tree/5.x) plugin. diff --git a/docs/6.x/extend/introduction.md b/docs/6.x/extend/introduction.md new file mode 100644 index 000000000..7b5441f7a --- /dev/null +++ b/docs/6.x/extend/introduction.md @@ -0,0 +1,23 @@ +# Prologue + +Laravel brings a fundamental shift in Craft’s relationship with a project, but plugin architecture remains largely the same. +Your plugin still has a single primary entry-point, registers features with Craft, defines HTTP and CLI APIs, and so on… + +Up to this point, Craft *was* the application—an extended instance of Yii, that owned the entire request lifecycle and acted as the “kernel” of the app. +Now, Craft is a *tenant* within a Laravel project, as are plugins. + +At a high level, Craft is one of many service providers that Laravel can discover. +We then expose a management and initialization layer that plugins use to register additional features. +The hierarchy is comparatively flat, though: as Laravel packages, plugins have virtually the same capabilities as Craft itself! + +Plugins aren’t the *only* way to extend Craft. +Since Craft 3.x, many developers have used *modules* to add functionality to individual projects or share it semi-privately between many. +You are still free to bundle this kind of extension into a package, or keep it as part of a single application. + +We are excited to share Craft’s new architecture and introduce plugin developers to Laravel conventions, within familiar territory. +When you have questions about organization, best practices, style, or new language features, we invite you to dive into Craft’s source (as you have in the past), or reach directly for the expansive Laravel documentation. + +The rest of this section covers common extension points from the perspective of existing Craft developers. + +Ready? +Let’s do it. diff --git a/docs/6.x/extend/local-dev.md b/docs/6.x/extend/local-dev.md new file mode 100644 index 000000000..2b2bc6661 --- /dev/null +++ b/docs/6.x/extend/local-dev.md @@ -0,0 +1 @@ +# Working Locally diff --git a/docs/6.x/extend/logging.md b/docs/6.x/extend/logging.md new file mode 100644 index 000000000..ad76e29a5 --- /dev/null +++ b/docs/6.x/extend/logging.md @@ -0,0 +1,17 @@ +# Logging + +`Craft::info()` and other log helpers should be replaced with Laravel’s [`Illuminate\Support\Facades\Log` facade](laravel:logging). + +## Deprecation Notices + +Use `CraftCms\Cms\Support\Facades\Deprecator::log()` to send deprecation notices to the **Deprecation Warnings** utility: + +```php +CraftCms\Cms\Support\Facades\Deprecator::log(__METHOD__, 'The track() Twig function is deprecated. Activity plugin APIs have been consolidated into the global `activity` variable: {% do activity.track(...) %}'); +``` + +You can log warnings for newly-deprecated APIs throughout 6.x, or within your compatibility layer. + +::: warning +Do not use the adapter’s deprecation API, as it may trigger its *own* warning! +::: diff --git a/docs/6.x/extend/macros.md b/docs/6.x/extend/macros.md new file mode 100644 index 000000000..2b9902578 --- /dev/null +++ b/docs/6.x/extend/macros.md @@ -0,0 +1,24 @@ +# Behaviors + +There is no direct equivalent to Yii’s [behavior](guide:behaviors) system, but classes that use `Illuminate\Support\Traits\Macroable` can be extended at runtime using [macros](laravel:macros). +Anything that extends our base component class (`CraftCms\Cms\Component\Component`) is “macroable.” +Define macros from your plugin’s `bootPlugin()` method: + +```php +use CraftCms\Cms\Site\Data\Site; + +public function bootPlugin() +{ + Site::macro('isB2b', function() { + return str_contains($this->handle, 'biz'); + }); +} +``` + +Then, wherever you’re using a site… + +```twig +{{ currentSite.isB2b() ? 'Welcome, Mr. Business!' : 'Hey, bud!' }} +``` + +Laravel does not have an instance-level event system, but you can colocate multiple event handlers in a [subscriber](laravel:events#event-subscribers). diff --git a/docs/6.x/extend/mail.md b/docs/6.x/extend/mail.md new file mode 100644 index 000000000..bc12a0126 --- /dev/null +++ b/docs/6.x/extend/mail.md @@ -0,0 +1,46 @@ +# Mail + +Mailer adapters are no longer necessary, as drivers are [configured directly via Laravel](laravel:mail), in `config/mailer.php`. + +## System Messages + +Craft’s **System Messages** utility is still used to manage built-in and plugin-provided email messages. + +Register a system message with the `RegisterSystemMessages` event, from your `bootPlugin()` method: + +```php +use Illuminate\Support\Facades\Event; +use CraftCms\Cms\SystemMessage\Data\SystemMessage; +use CraftCms\Cms\SystemMessage\Events\RegisterSystemMessages; + +Event::listen(function(RegisterSystemMessages $event) { + $event->messages->push(new SystemMessage( + key: 'report_finished', + heading: 'When a report is finished generating', + subject: 'Here is your {report.template.name} report', + body: "Hi, {report.creator.fullName}!\n\nA {report.template.name} just finished running. To download it, ...", + )); +}); +``` + +To send a system message, pass the “mailable” to Laravel’s `Mail` facade: + +```php +use CraftCms\Cms\SystemMessage\SystemMessages; +use Illuminate\Support\Facades\Mail; + +$message = app(SystemMessages::class)->mailable('report_finished', $report->creator, [ + 'report' => $report, +]); + +Mail::send($message); +``` + +## Other Mailables + +A system messages is just one implementation of Laravel’s `Mailable`, with the notable limitation of requiring an existing Craft user. +For all other email, extend our `CraftCms\Cms\Email\Mailables\CraftMailable` base class to apply site-specific mailer overrides. + +## Mail Events + +Register a [listener](events.md) for `Illuminate\Mail\Events\MessageSending` to monitor outgoing emails, and return `false` to suppress them. diff --git a/docs/6.x/extend/models.md b/docs/6.x/extend/models.md new file mode 100644 index 000000000..2e10d9e6b --- /dev/null +++ b/docs/6.x/extend/models.md @@ -0,0 +1,36 @@ +# Models, Records, and Data + +Craft 6.x adopts Laravel’s definition of a model, which is part of its database abstraction, the [Eloquent ORM](laravel:eloquent). +This means that there are some conceptual shifts for both models and records. + +## Database Records + +Many objects that were *records* in Craft 5.x are now Eloquent models, and extend `CraftCms\Cms\Shared\BaseModel`. +This implementation provides some familiar features, like standardized `dateCreated`, `dateUpdated`, and `dateDeleted` columns. +UIDs are optional, and come from `CraftCms\Cms\Shared\Concerns\HasUid`. + +Models are only concerned with the shape of data in the database—they no longer define methods or properties, use validation rules, emit events, etc. +We still use service-like classes to translate data objects to Eloquent models, for saving. + +This separation is _not_ required, for plugins; you are free to use `BaseModel` for anything that must be persisted in the database, while also implementing validation traits and interfaces! + +## Data Objects + +Most subclasses of `craft\base\Model` now extend `CraftCms\Cms\Component\Component`. +Components retain a lot of familiar features, like mass-assignment via constructors, automatic typecasting, [validation](validation.md), array access, behavior-like [macros](macros.md), and so on. + +You can also opt in to some component features, piecemeal: + +- `CraftCms\Cms\Validation\Contracts\Validatable` and `CraftCms\Cms\Validation\Concerns\Validates` together provide a full suite of [validation](validation.md) tools; +- The `#[\CraftCms\Cms\Validation\Ruleset()]` attribute lets you share sets of validation rules between objects; +- Any class can be safely “configured” (in its constructor, or from outside) using `CraftCms\Cms\Support\Typecast::configure()`; + + + +A big reason why we were able to pare down our own model implementations to generic “data transfer objects” has to do with Laravel’s validation engine, and our ability to validate data as it enters the system, rather than after it’s assigned to an object. + +In many situations, this ends up meaning that we can completely eliminate small, data-only models. + + + + diff --git a/docs/6.x/extend/provider.md b/docs/6.x/extend/provider.md new file mode 100644 index 000000000..4621b55f1 --- /dev/null +++ b/docs/6.x/extend/provider.md @@ -0,0 +1,47 @@ +# Base Plugin Class + +Plugins now extend `CraftCms\Cms\Plugin\Plugin`, and are Laravel [service providers](laravel:providers). + +Many plugins will only need to update their [settings model](#settings) and rename the `init()` method to `bootPlugin()`. +At this point, the plugin should be in a stable state from which you can gradually move [initialization logic](avenues.md#initialization) to the new architecture. + +::: tip +After switching classes (and making the corresponding change in `composer.json`), you will need to run `ddev artisan package:discover` for Laravel to locate and cache the plugin’s root service provider. +::: + +Your plugin’s `composer.json` file needs one new entry, `extra.laravel.providers`: + +```json{9-13} +{ + // ... + "extra": { + "handle": "_demo-plugin", + "name": "Demo Plugin", + "developer": "Pixel & Tonic", + "documentationUrl": "", + "class": "CraftCms\\DemoPlugin\\Demo", + "laravel": { + "providers": [ + "CraftCms\\DemoPlugin\\Demo" + ] + } + } +} +``` + +You are not required to adopt `PascalCase`. +This example reflects the change in convention in `extra.class`, as well—but the `class` field is only required if your base class is named something other than `Plugin`, in your autoloading root. + +## Settings + +When you change your plugin’s base class, its `createSettingsModel()` method signature will require that it return a `CraftCms\Cms\Validation\Contracts\Validatable` (or `null`) instead of a `craft\base\Model`. +We provide the abstract `CraftCms\Cms\Plugin\PluginSettings` class for you to extend. + +::: tip +Laravel tends to use *model* in the context of its Eloquent ORM. + +`PluginSettings` aren’t part of that system, so we’ve opted to move our own plugins’ settings classes to the same directory as the base plugin class. +This is not mandatory! +::: + +Read more about this in the [configuration and settings](config.md) section. diff --git a/docs/6.x/extend/queue.md b/docs/6.x/extend/queue.md new file mode 100644 index 000000000..af76c3e7c --- /dev/null +++ b/docs/6.x/extend/queue.md @@ -0,0 +1,52 @@ +# Jobs + Queue + +A job in Laravel is generally just a class with a `handle()` method. +Its behavior is determined by a handful of interfaces and traits, which we’ve packaged together for extension as `CraftCms\Cms\Queue\Job`. + +```php +namespace MyOrg\Activity\Reporting\Jobs; + +use CraftCms\Cms\Queue\Job; +use MyOrg\Activity\Reporting\Manager; + +class GenerateReport extends Job +{ + public function __construct( + public int $reportId, + public bool $notifyOwner, + ) {} + + // Laravel can automatically inject dependencies when it runs your job: + public function handle( + Manager $manager, + ): void + { + $report = $manager->getReportById($this->reportId); + } +} +``` + +Push jobs using the `dispatch()` helper: + +```php +dispatch(new GenerateReport(1234, true)); + +// The base Job class is `Dispatchable`, meaning it has an equivalent static method that forwards arguments to the constructor: +GenerateReport::dispatch(1234, true); +``` + +Counterintuitively, jobs are not necessarily queued! +When building a custom job, you’ll need to `use Illuminate\Contracts\Queue\ShouldQueue` and `use Illuminate\Queue\InteractsWithQueue;` to tell Laravel that it can (and _should_) be pushed to the queue, rather than executed immediately. + +## Priority + +Laravel’s queues do not have an equivalent for “priority,” but we allow individual apps to designate a `lowPriorityQueue` in their general config file. +You can divert nonessential jobs to that queue, with the understanding that they may not actually yield to the normal queue. +Communicate to developers about your use of the queue so they can plan accordingly. + +A few other changes are worth noting: + +- Jobs are now given a `dateCompleted` rather than being immediately deleted. Craft prunes the jobs table during garbage collection. +- `ttr` has been renamed `timeout` +- Job are identified by UUIDs instead of IDs. +- New kinds of jobs are available to projects that support them: consider implementing [chained jobs](laravel:queues#chains-and-batches), [collision-avoidance](laravel:queues#preventing-job-overlaps), or [parallelization](laravel:queues#job-batching) diff --git a/docs/6.x/extend/resources.md b/docs/6.x/extend/resources.md new file mode 100644 index 000000000..3c1229ee1 --- /dev/null +++ b/docs/6.x/extend/resources.md @@ -0,0 +1 @@ +# Resources diff --git a/docs/6.x/extend/services.md b/docs/6.x/extend/services.md new file mode 100644 index 000000000..17fe730e9 --- /dev/null +++ b/docs/6.x/extend/services.md @@ -0,0 +1,132 @@ +# Services + +::: tip +This section discusses one of the headiest *conceptual* changes in Craft 6.x, but requires very few *practical* changes to your codebase or architecture! +::: + +Services have been a staple of Craft and its plugin ecosystem from the beginning. +For most plugins, services *are* the API, and they were carefully grouped by, bound to, and accessed through “modules.” +This has taken basically two forms: + +```php +# Craft 2.x: +craft()->activityPlugin_reportsService->generate(...); + +# Craft 3.x–5.x: +Activity::getInstance()->getReports()->generate(...); +``` + +In Craft 6.x, “services” are nothing more than auto-loadable PHP classes. +Most are marked with the `Singleton` attribute to help Laravel’s service container reuse instances: + +```php +namespace MyOrg\Activity\Reporting; + +#[\Illuminate\Container\Attributes\Singleton] +class Manager +{ + public function generate(Template $template): Report + { + // ... + } +} +``` + +You’ll either access these directly through the service container… + +```php +app(\MyOrg\Activity\Reporting\Manager::class)->generate(...); +``` + +…or by adding a facade: + +```php +namespace MyOrg\Activity\Facades; + +use Illuminate\Support\Facades\Facade; + +class Reports extends Facade +{ + #[\Override] + protected static function getFacadeAccessor(): string + { + return \MyOrg\Activity\Reporting\Manager::class; + } +} + +# -> Reports::generate(...) +``` + +## Resolving a Service + +Craft’s internal structure and inheritance model has been radically flattened by adopting this pattern. +It also means that you don’t need to resolve every API through a central instance—in fact, you may find that you’re rarely reaching for the service locator, thanks to dependency injection… revisiting our controller example, from earlier: + +```php +namespace MyOrg\Activity\Http\Controllers; + +use Symfony\Component\HttpFoundation\Response; + +readonly class TrackEvent +{ + public function __construct( + public \Illuminate\Http\Request $request + ) {} + + public function handle( + MyOrg\Activity\Ledger\Events $events + ): Response + { + if (! $events->track($this->validated('name'))) { + abort(400, 'The event could not be tracked.'); + } + + return new JsonResponse(['success' => true]); + } +} +``` + +At no point in our method bodies do we need to manually “get” services from Craft (or even our own plugin). +The facade we created above brings dependency injection to the services they wrap, as well: + +```php +Reports::fork($template, $currentUser); +``` + +```php +class Manager +{ + public function fork( + Template $original, + ?User $creator = null, + Elements $elements, + ): Template + { + return $elements->duplicate($original, ['creator' => $creator ?? $original->creator]); + } +} +``` + +Outside a DI-capable context, you can always access Craft’s services via the `app()` container: + +```php +app(\CraftCms\Cms\ProjectConfig\ProjectConfig)->get('graphql.enabled'); +``` + +::: warning +You should never directly instantiate your services, especially those marked `#[Singleton]`. +The first time you request it through the container, Laravel will create an instance for you. +::: + +The structure of your services remains entirely up to you. +We used the opportunity to refactor a number of Craft’s core features—as an example, the garbage collection service (`Gc`) was reduced in size roughly 80% by splitting it into [a number of bite-size invokable “action” classes](https://github.com/craftcms/cms/tree/6.x/src/GarbageCollection/Actions) that all still have access to the main service (and any additional services it might need) via dependency injection. + +## Components + +To make the transition a bit easier, we’ve brought along some of Yii’s “component” features that were upstream of Craft services. +Your services (and other data models) can extend `CraftCms\Cms\Component\Component` to get access to validation, macros, array-style access, and more. + +## Plugin Getters + +Because your services are all accessible by their class names, plugins no longer need to call `setComponents([])`, or implement “getter” methods. +As a result, most calls to `Plugin::getInstance()` will be unnecessary (except for calling methods on the class itself). diff --git a/docs/6.x/extend/session.md b/docs/6.x/extend/session.md new file mode 100644 index 000000000..0d87b1cc9 --- /dev/null +++ b/docs/6.x/extend/session.md @@ -0,0 +1,80 @@ +# Session + +The session is deeply integrated with validation, templating, controllers, and authorization. Laravel is responsible for the majority of the session implementation, but Craft adds a few features via `CraftCms\Cms\Http\Mixins\SessionMixin`. + +## Auth + +You can always get the current `User` element with `Illuminate\Support\Facades\Auth::user()`. +Users are largely unchanged, but the class has been relocated to `CraftCms\Cms\User\Elements\User` and some of its functionality has been split out into concerns (or is handled entirely by Laravel). + +## Flashes + +You’re bound to need to communicate with the user about the status of a request. +Plugins may interact with the session directly (by flashing data to a session or reading it back out) and indirectly (via authorization, users, or permissions). + +The session is available anywhere you are working with a `Request` object (`$request→session()`), and via the global `session()` helper (in PHP and Twig). + +Craft always exposes an `errors` key in your templates, containing an instance of `Illuminate\Support\ViewErrorBag`. +For most responses, it will be empty; when a validation error occurs (say, triggered by `$request->validated('title')`, or in a `FormRequest`), Laravel adds those errors to that “bag” by field name: + +```php +{% if errors.has('title') %} +
    + {% for error in errors.get('title') %} +
  • {{ error }}
  • + {% endfor %} +
+{% endif %} +``` + +The message bag can *technically* hold multiple models’ errors, but Craft doesn’t use this feature. +As a result, there will be a single `default` bag, and calls to `errors.get()`, `has()`, and `any()` are forwarded to it. + +General flashes come in three categories: `success`, `notice`, and `error`. These are flashed separately from `errors` (plural), and will only contain a single message each: + +```html +{% set flashes = session().only(['success', 'notice', 'error']) %} + +{% if flashes is not empty %} +
    + {% for key, message in flashes %} +
  • {{ message }} ({{ key }})
  • + {% endfor %} +
+{% endif %} +``` + +This example only uses an array to simplify repeated access of the session object. +You can check each flash category separately by calling `session().get('success')`. +Within the control panel, this is all handled for you—including support for rich notification boxes with actions. + +We provide the static `CraftCms\Cms\Support\Flash` class to set and get these flash messages in a context-agnostic way. + +::: tip +If your controller includes the `CraftCms\Cms\Http\RespondsWithFlash` trait, all of its `as*` methods automatically handle flashing the passed message to the session. +::: + +To push more data to the session (like entire models), use the `RespondsWithFlash::asModelSuccess()` and `asModelFailure()` methods, or call `with()` on any redirection response (like `back()->with()` or `redirect()->with()`). When using `with()`, make sure your data is in a serializable state: + +```bash +return back() + ->with('error', t('This is already on your favorites list!')) + ->with([ + 'favorite' => CraftCms\Cms\Support\Arr::toArray($favorite), + ]); +``` + +### Other State + +The `old()` function is often used in conjunction with `errors` to access values as they were submitted: + +```php +{{ input('text', 'name', old('title')) }} + +# POST -> +``` + +::: danger +Be careful when echoing back user-submitted data. +Twig automatically escapes dynamic values (including those from `old()`), but it’s possible to expose your users to XSS vulnerabilities by using string interpolation or the `raw` filter. +::: diff --git a/docs/6.x/extend/templates.md b/docs/6.x/extend/templates.md new file mode 100644 index 000000000..495031c64 --- /dev/null +++ b/docs/6.x/extend/templates.md @@ -0,0 +1 @@ +# Templates + Rendering diff --git a/docs/6.x/extend/upgrade.md b/docs/6.x/extend/upgrade.md new file mode 100644 index 000000000..275f4671a --- /dev/null +++ b/docs/6.x/extend/upgrade.md @@ -0,0 +1,36 @@ +# Required Changes + +There are only a couple of things that you must do to get your plugin running in Craft 6.x. + +## Adapter + +Your plugin package should require `craftcms/yii2-adapter`: + +```bash +composer require craftcms/yii2-adapter +``` + +Additional work may be required to eject the [adapter](adapter.md). + +::: tip +The adapter is automatically installed by our [project upgrade tool](../upgrade.md), so it may _initially_ appear that your plugin works without it. +Similarly, the adapter will be active if _any_ Composer-installed plugin requires it! + +To test adequately, remove the adapter and all other plugins from the host project and require it only in your plugin. +::: + +## Signatures + +The legacy base plugin class (`craft\base\Plugin`) and the interfaces in implements may require signature updates for some lower-level methods. + +We have mainly observed this with the `attributes()` and `attributeLabels()` methods. +If you don’t use these, there’s nothing to do here. + +## Paths + +In general, we recommend keeping your primary plugin class within a `src/` directory at the root of your package. + +Plugin icons (`icon.svg` and `icon-mask.svg`) are expected to be in a new top-level `resources/` directory. +This is also typically where [static or publishable assets](assets.md) live; resources are *not* exposed in the web root, automatically. + +If you *must* use a different structure for your plugin package, define `basePath()` and/or `resourcesPath()` methods in your base plugin class so that Craft can locate your resources. diff --git a/docs/6.x/extend/validation.md b/docs/6.x/extend/validation.md new file mode 100644 index 000000000..3935fbe1e --- /dev/null +++ b/docs/6.x/extend/validation.md @@ -0,0 +1,2 @@ +# Validation + diff --git a/docs/6.x/install.md b/docs/6.x/install.md new file mode 100644 index 000000000..6e2c56610 --- /dev/null +++ b/docs/6.x/install.md @@ -0,0 +1,47 @@ +# Install + +The prevalence of modern, mature PHP development tools and infrastructure makes Craft easy to install, run, [upgrade](./upgrade.md), and [deploy](./deploy.md). + + + +This [quick-start](#quick-start) guide focuses solely on setting up a local Craft development environment to try out the alpha. +Downloading or installing Craft by any means (including alpha releases) binds you to its [license](https://craftcms.com/license). + +## Quick-Start + +Your journey with Craft begins on your local machine, using [DDEV](https://ddev.readthedocs.io/en/stable/). +DDEV is a Docker-based PHP development environment that streamlines the creation and management of resources required by a Craft project. + +[Install or update DDEV](https://ddev.readthedocs.io/en/stable/users/install/), then follow these steps: + +1. Create a project directory and move into it: + + ```bash + mkdir my-craft-project + cd my-craft-project/ + ``` + +1. Create DDEV configuration files: + + ```bash + # Note the new project type for 6.x! + ddev config --project-type=laravel --docroot=public --php-version=8.5 + ``` + +1. Scaffold the project from the official [starter project](https://github.com/craftcms/craft/tree/6.x): + + ```bash + ddev composer create-project "craftcms/craft@6" + ``` + + The setup wizard will start automatically! Accept all defaults (in `[square brackets]`), and note your chosen username and password. + + ::: tip + Our [First-Time Setup](kb:first-time-setup) guide in the Knowledge Base has more information about what to expect during setup. + ::: + +Congratulations! You now have a fully-functional Craft application installed and configured. Run `ddev launch` to view the starter project’s welcome screen. + +::: tip +We have a list of recommended next steps for new Craft users in the [5.x installation guide](/5.x/install.md). +::: diff --git a/docs/6.x/requirements.md b/docs/6.x/requirements.md new file mode 100644 index 000000000..a5ebbbad8 --- /dev/null +++ b/docs/6.x/requirements.md @@ -0,0 +1,142 @@ +--- +description: Craft requires PHP 8.5 and a MySQL or Postgres database. +--- + +# Requirements + +Craft is a PHP application that uses a relational database for content storage. It will run on most modern hosting environments, and can be configured to take advantage of all kinds of advanced infrastructure. + + + +::: tip +You can use the official [server check](https://github.com/craftcms/server-check) script to quickly find out if your server meets Craft’s requirements. +::: + + + + +## Minimum System Specs + +- PHP 8.5+ +- MySQL 8.0.17+ using InnoDB, MariaDB 10.4.6+, or PostgreSQL 13+ +- 256MB+ memory allocated to PHP +- 200MB+ free disk space +- Composer 2.0+ + + + + +## Recommended System Specs + +- PHP 8.2+ +- MySQL 8.0.36+ using InnoDB, or PostgreSQL 16+ +- 512MB+ of memory allocated to PHP + + + + +::: warning +Due to its diverging parity with MySQL, we no longer recommend MariaDB for sites with many users or a large volume of content. +::: + +Partial (and _experimental_) support has been added for SQLite 3.38.0+. +We will make a determination about official support as we approach a general release. + +## Required PHP Extensions + +- [BCMath](https://www.php.net/manual/en/book.bc.php) +- [ctype](https://secure.php.net/manual/en/book.ctype.php) +- [cURL](http://php.net/manual/en/book.curl.php) +- [GD](http://php.net/manual/en/book.image.php) +- [iconv](http://php.net/manual/en/book.iconv.php) +- [Intl](http://php.net/manual/en/book.intl.php) +- [JSON](http://php.net/manual/en/book.json.php) +- [Multibyte String](http://php.net/manual/en/book.mbstring.php) +- [OpenSSL](http://php.net/manual/en/book.openssl.php) +- [PCRE](http://php.net/manual/en/book.pcre.php) +- [PDO MySQL Driver](http://php.net/manual/en/ref.pdo-mysql.php) or [PDO PostgreSQL Driver](http://php.net/manual/en/ref.pdo-pgsql.php) +- [PDO](http://php.net/manual/en/book.pdo.php) +- [Reflection](http://php.net/manual/en/class.reflectionextension.php) +- [SPL](http://php.net/manual/en/book.spl.php) +- [Zip](http://php.net/manual/en/book.zip.php) +- [DOM](http://php.net/manual/en/book.dom.php) + +::: tip +We recommend [ImageMagick](http://php.net/manual/en/book.imagick.php) over GD for expanded [image handling options](/5.x/development/image-transforms.md). + +When multiple image drivers are available on your platform, Craft prefers ImageMagick. +You can explicitly select a driver with the setting. +::: + +## Optional PHP Methods and Configurations + +Some shared hosting environments disable common PHP methods and configurations that affect Craft features. + +- [allow_url_fopen](http://php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen) — This `ini` setting must be enabled to support updating and installing plugins from the Plugin Store. +- [proc_*](http://php.net/manual/en/ref.exec.php) — These methods are required in order to utilize the Plugin Store, install updates, and send emails. +- [ignore_user_abort](https://www.php.net/manual/en/function.ignore-user-abort.php) — Required when using the default [web-based queue runner](system/queue.md#http) to operate. +- [pcntl_*](https://www.php.net/manual/en/book.pcntl.php) — The _Process Control_ extension allows [daemonized queue runners](system/queue.md#daemon) to gracefully exit between jobs. + +## File Permissions + +For Craft to run properly, PHP needs to be able to write to the following files and folders at all times: + +- `storage/*` +- `config/license.key` +- `public/cpresources/*` + +Additionally, during [setup](kb:first-time-setup) or when [updating](/5.x/update.md) or installing [plugins](/5.x/system/plugins.md) via the [control panel](/5.x/system/control-panel.md) or [CLI](/5.x/system/cli.md), Craft may touch these files: + +- `.env` +- `config/craft/project/*` +- `composer.json` and `composer.lock` +- `vendor/*` + +The exact permissions depend on the relationship between the system user that PHP runs as and the owner of the folders and files: + +- If they’re the same user, use `744` (`rwxr--r--`). +- If they’re in the same group, use `774` (`rwxrwxr--`). +- If neither of the above options describe your setup, something may have been misconfigured. Reach out to your system administrator for support. + +Specifics may vary from platform to platform or host to host! Consult your development or hosting environment’s documentation for more information. + +::: danger +**Never** set permissions to `777` in a shared environment or on a live site, and **never** run your HTTP server (or PHP) as `root`. +::: + +## Required Database User Privileges + +The database user you tell Craft to connect with must have the following privileges: + + + + +### MySQL/MariaDB + +- `SELECT` +- `INSERT` +- `DELETE` +- `UPDATE` +- `CREATE` +- `ALTER` +- `INDEX` +- `DROP` +- `REFERENCES` +- `LOCK TABLES` + + + + + +#### PostgreSQL + +- `SELECT` +- `INSERT` +- `UPDATE` +- `CREATE` +- `DELETE` +- `REFERENCES` +- `CONNECT` + + + diff --git a/docs/6.x/upgrade.md b/docs/6.x/upgrade.md new file mode 100644 index 000000000..600bd7083 --- /dev/null +++ b/docs/6.x/upgrade.md @@ -0,0 +1,331 @@ +--- +description: Craft 6.x is our biggest technical leap and easiest upgrade, yet. Let’s get started! +sidebarDepth: 3 +--- + +# Upgrading to Craft 6.x + +The smoothest way to upgrade to Craft 6 is to make sure your live and local environments are already running the [latest version of Craft 5](/5.x/updating.md). +We recommend approaching the upgrade in three phases: [preparation](#preparing-for-the-upgrade), a [local upgrade](#performing-the-upgrade), and [triage](#deprecations). + + + +## Preparing for the Upgrade + +### Requirements + +There’s a few things that you need to take care of _before_ the upgrade (even with the [adapter](#adapter)): + +- Update Craft to the latest 5.x release, and all plugins to their latest compatible versions (see the note below about the state of plugins during alpha). You cannot upgrade directly to Craft 6.x from Craft 4.x or earlier. +- Upgrade PHP on your host to 8.5 +- Resolve any outstanding deprecation notices +- Flatten any [multi-environment](/5.x/configure.md#multi-environment-configs) config files (arrays with a `*` key and one or more environment names) using environment variables. If you use the [fluent style](/5.x/configure.md#style) for configuration, you are good to go. + +### Reminders + +The alpha is primarily intended to give [plugin developers](extend/README.md) a chance to make a handful of required compatibility updates. +That said, we were seeing successful upgrades as early as the first developer preview, which we shared at Dot All Lisbon, in October 2025! + +If you haven’t already, consider spinning up a fresh Craft 6.x project. +You’ll be able to get a sense for the new project structure without the pressure of immediately + +- If you want to customize how Categories, Global Sets, and Tags are migrated, use the [`entrify/*` commands](/5.x/reference/cli.md#entrify) before you begin the upgrade. +- Commit and deploy any final changes to your live environment + - While project config schema is *mostly* consistent between 5.x and 6.x, we do not recommend attempting to merge changes across versions (i.e. from an a feature branch), especially during the alpha and beta. +- Allow your queue to fully process. Consider running `ddev craft queue/run` if there are a lot of pending jobs and you can’t keep a browser open. + ::: warning + During the upgrade, your `queue` table is removed and rebuilt. _Any remaining jobs will be lost._ + ::: +- Capture a database backup. +- Take note of your configured mail adapter in . + +Your project is apt to continue working after the update, but it will include the adapter package. +Don’t remove it before reviewing the rest of this guide! + +### On Plugins + +During the alpha and beta phases, you may need to set `CRAFT_DISABLED_PLUGINS="*"` in `.env` or fully uninstall plugins before starting the upgrade to prevent incompatible ones from loading or running. +They are bootstrapped very early in the app’s lifecycle, and an error can leave your project in a partially-upgraded state. + +Not all plugins will be necessary! +Laravel makes it possible to directly configure mailers, loggers, filesystems, and more. + +## Performing the Upgrade + +1. Install the upgrade tool: + + ```bash + composer global install craftcms/craft6-revamp + ``` + + The tool will examine your project structure and will warn you if it’s unable to safely make changes. + It runs on any system that runs Craft 5.x. + + ::: tip + If you can’t install a composer package on your host machine, you can mount your project directory into an ephemeral Docker container and run the commands there: + + ``` + # On the host machine... + $ docker run --interactive --tty --volume $PWD:/app composer bash + + # ...in the container: + -> 42dae745a6ab:/app# composer global require craftcms/craft6-revamp + -> ... + -> 42dae745a6ab:/app# composer global exec craft6-revamp + ``` + ::: +2. Run `craft6-revamp` in your project’s root directory. The full list of actions our tool will attempt to take is available on its [repository](repo:craftcms/craft6-revamp). +3. Perform any **Next Steps** recommended by the tool. At a minimum, this should include… + - `ddev restart` to apply the new project settings (DDEV users only); + - `ddev composer update` to install new dependencies (or `composer update` outside of DDEV); + - `ddev artisan vendor:publish --tag=craftcms` to publish stubs for Laravel and Craft; + +### Run Migrations + +There are about ten lightweight migrations to run. +Many handle replacing legacy class names with their new namespaces. +None touch your content, and the number of required queries does not scale with the amount of elements you have. + +``` +# Use the new `artisan` entrypoint... +ddev artisan craft:up + +# ...or the familiar `craft` executable: +ddev php craft up +``` + +The new DDEV project type means that it will only forward `artisan` commands, but you can still use Craft’s entry point via `php`. + +::: tip +If you see a warning about the application being in production, it’s safe to ignore. +Laravel will warn you before applying migrations when `APP_DEBUG` is off. +::: + + + +### Cleanup + +We recommend taking another snapshot of your database and code so that you can revert to this freshly-upgraded state if you want to try different strategies for dealing with [deprecations](#deprecations). + + +## Deprecations + +At this point, you should have a fully-functional Craft application! + +The rest of the upgrade can be tackled at your leisure, but you should review the rest of this section to get an idea of how much work is ahead. + +### Bootstrapping + +During the upgrade, we removed the `bootstrap.php` file that has come with new projects [since Craft 3.7](https://github.com/craftcms/craft/releases/tag/1.1.5). +If you had any customizations to Craft’s initialization process (like how environment variables are loaded), you may need to find equivalent features in Laravel. + +Many of the [bootstrap variables](https://craftcms.com/docs/5.x/reference/config/bootstrap.html) you would define here have been removed and will have no effect. +We strongly recommend using the [new default Craft project structure](https://github.com/craftcms/craft/tree/6.x). + +If you used the upgrade tool, it removed `vlucas/phpdotenv` from your `composer.json`, but it will still be installed as a transitive dependency of Laravel. +You do not need to do anything to continue using a `.env` file. + +### Configuration + +### General Config + +As mentioned in the preparation step, multi-environment configuration is no longer supported. + +A few settings have been removed, renamed, or relocated: + +| Setting | Notes | +| --- | --- | +| `timezone` | Use the `timezone` key in the main Laravel config file (`config/app.php`). Project config supersedes this. | +| `defaultCookieDomain` | Use the `domain` key in Laravel’s [session](laravel:session#configuration) config file (`config/session.php`). | +| `blowfishHashCost` | Use `blowfish.bcrypt.rounds` in Laravel’s [hashing](laravel:hashing#main-content) config file (`config/hashing.php`). | +| `phpSessionName` | Use `cookie` in Laravel’s `config/session.php` | +| `systemMessageTemplate` | Site-specific templates are configured via Settings → Email | +| `elevatedSessionDuration` | Use [`password_timeout`](laravel:authentication#password-confirmation) in Laravel’s authentication config file (`config/auth.php`). The default is now 10800 seconds (three hours). | + + +You can selectively publish Laravel’s default config files from the console: + +```bash +ddev artisan config:publish [auth|session|hashing|mail|...] +``` + +### Database + +`db.php` is no longer used. +Environment variables beginning with `CRAFT_DB_*` in `.env` have been renamed to agree with [Laravel convention](laravel:database#configuration) and the new DDEV project type behavior. + +Advanced configuration (historically done via `app.php`) can be accomplished with Laravel’s `config/database.php`. + +### Control Panel + +Some sections in the control panel have been replaced by direct Laravel configuration. + +#### Email + +Configure Laravel’s [mailer](laravel:mail) using `config/mail.php`. + +- Translate your old mail adapter’s settings into the appropriate config array, under `mailers`. +- Set the `default` near the top of `mail.php` to your chosen driver. The default configuration works with DDEV’s Mailpit service. +- Optional: Remove any drivers you don’t want/need. +- Optional: Configure additional `failover` drivers. +- Test from the control panel () or console (`ddev artisan craft:mailer:test`). + +::: tip +Password reset and email validation notifications are now sent via the queue. +::: + +#### Branding + +Two new general config settings are available, which replace customizations you would make via the control panel in . +`cpIconUrl` and `cpLogoUrl` can be set to any string that resolves to a publicly-accessible url: + +- `/logo.png` +- `asset('logo.png')` +- `env('LOGO_URL')` +- `https://...` +- Aliases (e.g: `@brand/logo.png`) + +::: tip +Control panel branding is now available to all editions (Solo, Team, and Pro)! +::: + +### Templates + +Your templates have been moved to `resources/views/`, per Laravel convention. + +#### Flashes + +The way you access flashes and restore submitted data has changed. +A global `errors` variable will be populated when a model fails validation. + +Access individual field errors from that object using `errors.has('fieldName')` and `errors.get('fieldName')`. +This snippet is equivalent to the macro in our [entry form guide](kb:entry-form) and can be used similarly to the [forms documentation](/5.x/development/forms.md#models-and-validation) on models and validation: + +```twig +{% macro errorList(errors, field) %} + {% if errors.has(field) ?? false %} +
    + {% for error in errors.get(field) %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +{% endmacro %} +``` + +Submitted values are flashed back to the session and can be retrieved using the `old('fieldName')` Twig helper, after redirection. + +#### HTML Purification + +HTMLPurifier has been replaced by Symfony’s [HtmlSanitizer](https://symfony.com/doc/current/html_sanitizer.html). +This means any custom configurations in `config/craft/htmlpurifier/*` will need to be translated into [the new format](https://symfony.com/doc/current/html_sanitizer.html#configuration), and registered via a service provider (or contributed by plugin). + +::: tip +If you never modified Craft’s `Default.json` config, you can just remove it. +The new defaults are intended to be equivalent. +::: + +The `purify` Twig filter has also been replaced with the appropriately-named `sanitize` filter. +Like the old one, this filter accepts a custom configuration handle. + + +#### Markdown + +We also replaced the Markdown engine that came with Yii (`cebe/markdown`) with [CommonMark](https://commonmark.thephpleague.com/). +All the same filters and flavors remain available, but customizations to the parser may not work. + +If you were parsing Markdown anywhere in PHP (like an [Element API](plugin:element-api) transformer), you should use the new facade: + +```php +use CraftCms\Cms\Support\Facades\Markdown; + +Markdown::parse($text); +Markdown::parseParagraph($line); +``` + +### Commands + +Craft’s `exec` command has been removed. +We recommend using [Laravel’s `tinker` REPL](https://github.com/laravel/tinker) as a replacement for this and Yii’s `shell` command. + +If you still find the need to evaluate arbitrary PHP code in a non-interactive setting, you can re-implement it as a [closure command](laravel:artisan#closure-commands) on a project-by-project basis, from a service provider: + +```php +public function boot() +{ + Artisan::command('eval {code}', function ($code) { + $output = ''; + + $this->components->task( + 'Evaluating PHP code', + function () use ($code, &$output) { + ob_start(); + eval($code); + $output = ob_get_clean(); + } + ); + + $this->line($output); + + return 0; + }); +} +``` + +### Routing + +The `{uid}` placeholder token now matches UUIDs of any version. +This only means that the rule may be *more* permissive; your routes will continue to work as expected. + +## Extensions + +Plugin developers should continue exploring the [extension upgrade guide](extend/README.md). + +If you maintain a _module_, your application just got a whole lot more powerful. +Craft 6.x projects are just Laravel applications, meaning you are free to use them just as you would if Craft wasn’t part of the picture… except you get access to its huge library of APIs for working with content, users, templates, GraphQL, and more! + +You can follow along with the plugin development guide or jump right in to the [Laravel documentation](laravel:lifecycle) to learn about how modules can unfurl into your project’s main “app” space. + +## Tools + Resources + +Everything we’ve talked about, so far. + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +