From b43459cf6f41f250878e298d978b92fa8803a8d0 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Fri, 16 Oct 2026 14:46:27 +0800 Subject: [PATCH 1/2] 6.x extend: fix inaccuracies found porting craftcms/guest-entries Verified against craftcms/cms 6.0.0-alpha.20 while porting a plugin: - provider.md: createSettings() is static; warn that switching to CraftCms\Cms\Plugin\Plugin stops the adapter routing Yii controllers, and that settingsHtml() is replaced by settingsForm(). - session.md, http.md: Auth::craftUser() does not exist; use $request->craftUser() or the currentUser()/currentUserElement() helpers. - http.md: document the CSRF opt-out (PreventRequestForgery::except()). - upgrade.md: PluginSettings has no attributes() to call parent:: on. - Fix broken links in approach.md, commands.md and config.md, and an unfinished comment in local-dev.md. --- docs/6.x/extend/approach.md | 2 +- docs/6.x/extend/commands.md | 4 ++-- docs/6.x/extend/config.md | 2 +- docs/6.x/extend/http.md | 23 ++++++++++++++++++++++- docs/6.x/extend/local-dev.md | 2 +- docs/6.x/extend/provider.md | 16 ++++++++++++++++ docs/6.x/extend/session.md | 2 +- docs/6.x/extend/upgrade.md | 6 ++++++ 8 files changed, 50 insertions(+), 7 deletions(-) diff --git a/docs/6.x/extend/approach.md b/docs/6.x/extend/approach.md index a4d9f3f3d..df8f4cfbe 100644 --- a/docs/6.x/extend/approach.md +++ b/docs/6.x/extend/approach.md @@ -15,7 +15,7 @@ Some low-level plugins may no longer be necessary in the Laravel ecosystem, but 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. +- **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](laravel:logging) tools. ::: warning Filesystems have been completely removed in Craft 6.x. diff --git a/docs/6.x/extend/commands.md b/docs/6.x/extend/commands.md index ef6ec06e8..f54e91056 100644 --- a/docs/6.x/extend/commands.md +++ b/docs/6.x/extend/commands.md @@ -45,7 +45,7 @@ class GenerateReport extends Command } ``` -Your command’s [arguments and options](artisan#defining-input-expectations) are determined by its _signature_. +Your command’s [arguments and options](laravel: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. @@ -59,7 +59,7 @@ public array $commands = [ ## Command Closure -You can also create simple commands directly in your plugin’s `boot()` method, using a [laravel:artisan#closure-commands]: +You can also create simple commands directly in your plugin’s `boot()` method, using a [closure command](laravel:artisan#closure-commands): ```php Artisan::command('report:generate {templateId}', function (Manager $manager, int $templateId) { diff --git a/docs/6.x/extend/config.md b/docs/6.x/extend/config.md index fbbb51fcd..de10099ee 100644 --- a/docs/6.x/extend/config.md +++ b/docs/6.x/extend/config.md @@ -134,7 +134,7 @@ public function rulesClass(): string ``` 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](validation#form-request-validation) class from `getSettingsRequestClass()`. +For more control over how your settings data is normalized, return your own [form request](validation.md#form-request-validation) class from `getSettingsRequestClass()`. When using a custom `FormRequest`, your rules are evaluated against the entire request, not just the nested `settings` key that Craft ordinarily assigns to your settings model. The example above (validating `adminNotificationsAddress` on the settings model) would need to be updated to `settings.adminNotificationsAddress` if returned from `FormRequest::rules()`. diff --git a/docs/6.x/extend/http.md b/docs/6.x/extend/http.md index 6473072d4..3b9192b4d 100644 --- a/docs/6.x/extend/http.md +++ b/docs/6.x/extend/http.md @@ -80,6 +80,27 @@ Laravel’s “terminable” middleware is not compatible with all hosting envir app()->terminating($this->flushLazyEvents()); ``` +### CSRF + +Laravel verifies CSRF tokens on web routes, so a form posted without a token is rejected with a 419. +Front-end templates can keep using `{{ csrfInput() }}`, and Ajax requests can send the `XSRF-TOKEN` cookie value in an `X-XSRF-TOKEN` header. + +If an endpoint must accept posts without a token (a webhook, or a plugin that used to disable `enableCsrfValidation` on a Yii controller), exempt its URI from your plugin’s `boot()` method: + +```php +use CraftCms\Cms\Route\Routes; +use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; + +public function boot(Routes $routes): void +{ + PreventRequestForgery::except([ + $routes->actionTriggerUriPrefix()."/$this->handle/save", + ]); +} +``` + +Craft uses the same mechanism for its own `graphql/api` action. + ### Authorization Controllers in Laravel don’t often do their own authorization, because they’re no longer automatically exposed to the router. @@ -209,7 +230,7 @@ In some situations, you may still need to resolve permissions directly via a use ```php // Check against the current user: -Auth::craftUser()->can('triggerReport'); +currentUserElement()->can('triggerReport'); // Check against a specific user: $recipient->can('requestAccess', $report); diff --git a/docs/6.x/extend/local-dev.md b/docs/6.x/extend/local-dev.md index 16cc33848..6701d5ba5 100644 --- a/docs/6.x/extend/local-dev.md +++ b/docs/6.x/extend/local-dev.md @@ -6,7 +6,7 @@ If the host project doesn’t include the recommended `scripts` for a Laravel ap ```bash ddev composer require myorg/activity-plugin -# New: Ask Laravel to +# New: Ask Laravel to discover the package ddev artisan package:discover ``` diff --git a/docs/6.x/extend/provider.md b/docs/6.x/extend/provider.md index b0a812e83..b17ec49e6 100644 --- a/docs/6.x/extend/provider.md +++ b/docs/6.x/extend/provider.md @@ -7,6 +7,13 @@ Plugins now extend `CraftCms\Cms\Plugin\Plugin`, and are Laravel [service provid Many plugins will only need to update their [settings model](#settings) and rename the `init()` method to `boot()`. At this point—with the adapter installed—your plugin should be in a stable state from which you can gradually move [initialization logic](avenues.md#initialization) to the new architecture. +::: warning +The adapter only routes `actions/{handle}/…` URLs to plugins that extend the legacy `craft\base\Plugin` class. +Once your plugin extends `CraftCms\Cms\Plugin\Plugin`, any Yii controllers it ships return a 404 until you port them to Laravel controllers and [route](http.md#action-paths) them in `routes/actions.php`. +Switch the base class together with your controllers, and test your action URLs afterwards. +The settings page also changes: `settingsHtml()` is not used by the new base class, so implement `settingsForm()` (see [settings](config.md#settings-form)). +::: + You are not obligated to adopt `PascalCase` namespaces, but if you do (and your base class is named something other than `Plugin`, in your autoloading root), make the corresponding update to the `extra.class` property in `composer.json`: ```json{8} @@ -27,6 +34,15 @@ In order to support the Craft plugin lifecycle (installation and uninstallation, ## Settings The `createSettings()` method replaces `createSettingsModel()`, and must return a subclass of `CraftCms\Cms\Plugin\PluginSettings` (or `null`). +Note that it is a **static** method: + +```php +protected static function createSettings(): ?PluginSettings +{ + return new Settings(); +} +``` + This base class retains much of the same [component](models.md#data-objects) functionality, and should be a drop-in replacement for `craft\base\Model`. ::: tip diff --git a/docs/6.x/extend/session.md b/docs/6.x/extend/session.md index b14ec49f8..8f9d1958b 100644 --- a/docs/6.x/extend/session.md +++ b/docs/6.x/extend/session.md @@ -4,7 +4,7 @@ The session is deeply integrated with validation, templating, controllers, and a ## Auth -You can always get the current `User` element with `Illuminate\Support\Facades\Auth::craftUser()`. +You can always get the current user with the `craftUser()` method on the request (`$request->craftUser()`), or the `CraftCms\Cms\currentUser()` and `CraftCms\Cms\currentUserElement()` helper functions. 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 diff --git a/docs/6.x/extend/upgrade.md b/docs/6.x/extend/upgrade.md index 1b5939b8e..2aee5d505 100644 --- a/docs/6.x/extend/upgrade.md +++ b/docs/6.x/extend/upgrade.md @@ -26,6 +26,12 @@ The legacy base plugin class (`craft\base\Plugin`) and the interfaces it impleme We have mainly observed this with the `attributes()` and `attributeLabels()` methods. If you don’t use these, there’s nothing to do here. +::: tip +This applies while you keep extending `craft\base\Plugin` and `craft\base\Model`. +The new `CraftCms\Cms\Plugin\PluginSettings` class extends `CraftCms\Cms\Component\Component`, which has no `attributes()` method, so a settings class that moves to it can’t call `parent::attributes()`. +Expose values as public properties instead. +::: + Basically, _yes_! From 27b375d2df62f8f7ceba44cbd1cf61765b06a433 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Fri, 16 Oct 2026 15:40:05 +0800 Subject: [PATCH 2/2] 6.x extend: recommend a Craft-style class organization Adds a Class Organization section to structure.md, mirroring Craft core (Http/Controllers, Events, Data, Models, ...), for plugins ported from Craft 5. Co-Authored-By: Claude Sonnet 5.5 --- docs/6.x/extend/structure.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/6.x/extend/structure.md b/docs/6.x/extend/structure.md index 3a400bde4..8ab17a773 100644 --- a/docs/6.x/extend/structure.md +++ b/docs/6.x/extend/structure.md @@ -9,6 +9,31 @@ Within your plugin’s `src/` directory, you are not obligated to use any partic While some conventions are similar, Laravel’s opinionated paths (i.e. `Listeners/` for auto-registered [event](events.md) handlers) are only relevant in the project’s space (`app/`), and have no bearing on plugins. ::: +## Class Organization + +You are free to organize `src/` however you like, but we recommend following the conventions that Craft’s own code (and most Laravel packages) use: a PascalCase PSR-4 namespace (like `CraftCms\MyPlugin`), with classes grouped by what they _are_. +Craft groups by feature first, and by role within each feature. + +| Kind of class | Location | Core example | +| --- | --- | --- | +| Plugin class and settings | `src/Plugin.php`, `src/Settings.php` | | +| Controllers | `src/Http/Controllers/` | `CraftCms\Cms\Http\Controllers\AddressesController` | +| Form requests | `src/Http/Requests/` | | +| Middleware | `src/Http/Middleware/` | | +| Events | `src/Events/` | `CraftCms\Cms\Section\Events\SectionSaved` | +| Listeners | `src/Listeners/` | | +| Data objects | `src/Data/` | `CraftCms\Cms\Section\Data\Section` | +| Eloquent models (formerly records) | `src/Models/` | `CraftCms\Cms\Section\Models\Section` | +| Enums | `src/Enums/` | `CraftCms\Cms\Section\Enums\SectionType` | +| Exceptions | `src/Exceptions/` | `CraftCms\Cms\Section\Exceptions\SectionNotFoundException` | +| Jobs, commands, policies | `src/Jobs/`, `src/Console/Commands/`, `src/Policies/` | | +| Services | A feature folder, with `#[Singleton]` classes | `CraftCms\Cms\Section\Sections` | + +Larger plugins can nest these inside a feature folder (`src/Reporting/Events/`, `src/Reporting/Data/`). + +When porting a Craft 5 plugin, this usually means moving classes out of Yii-style lowercase folders (`controllers/`, `models/`, `events/`, `services/`) as you port them. +Directory names are case-sensitive, so make sure each namespace matches its path exactly, and list every renamed class in your upgrade notes. + ## Plugin Base Class Craft needs to be able to locate your base class.