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/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.
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_!