diff --git a/docs/README.md b/docs/README.md
index 45d90f93e..c4acd4179 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,7 +6,7 @@ slug: /
Flarum is a delightfully simple discussion platform for your website. It's fast, free, and easy to use, with all the features you need to run a successful community. It's also extremely extensible, allowing for ultimate customizability.
-![Flarum Home Screenshot](/en/img/home_screenshot.png)
+![Flarum Home Screenshot](./assets/home_screenshot.png)
## Goals
@@ -16,7 +16,7 @@ Flarum is the combined successor of [esoTalk](https://github.com/esotalk/esoTalk
* **Beautiful and responsive.** This is forum software for humans. Flarum is carefully designed to be consistent and intuitive across platforms, out-of-the-box.
-* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarum’s architecture is amazingly flexible, with a [powerful Extension API](/extend/README.md).
+* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarum’s architecture is amazingly flexible, with a [powerful Extension API](./extend/README.md).
* **Free and open.** Flarum is released under the [MIT license](https://github.com/flarum/flarum/blob/master/LICENSE).
diff --git a/docs/admin.md b/docs/admin.md
index c412a14b7..4956589bc 100644
--- a/docs/admin.md
+++ b/docs/admin.md
@@ -11,5 +11,6 @@ The Admin Dashboard has the following sections, being:
- **Permissions** - Shows the permissions for each user group, and allows you to configure global and specific scopes.
- **Appearance** - Allows you to customize the forum's colors, branding and add additional CSS for customization.
- **Users** - Provides you with a paginated list of all the users in the forum, and grants you the ability to edit the user or take administrative actions.
+- **Advanced** - Allows you to configure advanced settings such as Maintenance Mode, Search drivers, and more.
-Apart from the above mentioned sections, the Admin Dashboard also allows you to manage your Extensions (including the flarum core extensions such as Tags) under the _Features_ section. Extensions which modify the forum theme, or allow you to use multiple languages are categorized under the _Themes_ and _Languages_ section respectively.
+Apart from the above-mentioned sections, the Admin Dashboard also allows you to manage your Extensions (including the flarum core extensions such as Tags) under the _Features_ section. Extensions which modify the forum theme, or allow you to use multiple languages are categorized under the _Themes_ and _Languages_ section respectively.
diff --git a/docs/assets/api_flowchart.svg b/docs/assets/api_flowchart.svg
new file mode 100644
index 000000000..04921c3ad
--- /dev/null
+++ b/docs/assets/api_flowchart.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/docs/assets/config-repositories.png b/docs/assets/config-repositories.png
new file mode 100644
index 000000000..24f6ffb68
Binary files /dev/null and b/docs/assets/config-repositories.png differ
diff --git a/docs/assets/extension-manager-page.png b/docs/assets/extension-manager-page.png
new file mode 100644
index 000000000..8a73ee160
Binary files /dev/null and b/docs/assets/extension-manager-page.png differ
diff --git a/docs/assets/extension-manager-queue.png b/docs/assets/extension-manager-queue.png
new file mode 100644
index 000000000..523665afb
Binary files /dev/null and b/docs/assets/extension-manager-queue.png differ
diff --git a/docs/assets/gambit_autocomplete_dropdown.png b/docs/assets/gambit_autocomplete_dropdown.png
new file mode 100644
index 000000000..be97d30b8
Binary files /dev/null and b/docs/assets/gambit_autocomplete_dropdown.png differ
diff --git a/docs/assets/global_search_modal.png b/docs/assets/global_search_modal.png
new file mode 100644
index 000000000..f700d8405
Binary files /dev/null and b/docs/assets/global_search_modal.png differ
diff --git a/docs/assets/home_screenshot.png b/docs/assets/home_screenshot.png
new file mode 100644
index 000000000..1bb9cb2aa
Binary files /dev/null and b/docs/assets/home_screenshot.png differ
diff --git a/docs/assets/install-extension.png b/docs/assets/install-extension.png
new file mode 100644
index 000000000..08000b9ed
Binary files /dev/null and b/docs/assets/install-extension.png differ
diff --git a/docs/assets/uninstall-extension.png b/docs/assets/uninstall-extension.png
new file mode 100644
index 000000000..4a4e1404d
Binary files /dev/null and b/docs/assets/uninstall-extension.png differ
diff --git a/docs/assets/update-extension.png b/docs/assets/update-extension.png
new file mode 100644
index 000000000..bc0586e7b
Binary files /dev/null and b/docs/assets/update-extension.png differ
diff --git a/docs/bugs.md b/docs/bugs.md
index 85e8ea193..1a486d832 100644
--- a/docs/bugs.md
+++ b/docs/bugs.md
@@ -13,7 +13,7 @@ Thank you for helping us test Flarum. We're happy to have you on the team! We ne
Found a bug already? Wonderful! We'd love to hear about it — but first you should check around to make sure you're not wasting your time on a known issue:
- Search our [Support forum](https://discuss.flarum.org/t/support) to see if it's already been reported.
-- We could be working on a fix, so search our [issue tracker](https://github.com/flarum/core/issues) too.
+- We could be working on a fix, so search our [issue tracker](https://github.com/flarum/framework/issues) too.
If you've searched *thoroughly* and come up empty-handed, we'll welcome your report. If it's just a simple issue (a misspelled word or graphics glitch, for example) skip to the next paragraph. But if you're seeing errors, or something is clearly broken, we'll need you to gather some information first. Please head over to our [Troubleshooting](troubleshoot.md) guide and follow the instructions there. Collect as much info as you can!
diff --git a/docs/config.md b/docs/config.md
index 2b23101e9..fdfa195cf 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -12,10 +12,10 @@ Here's a quick overview of what everything means with an example file:
```php
false, // enables or disables debug mode, used to troubleshoot issues
- 'offline' => false, // enables or disables site maintenance mode. This makes your site inaccessible to all users (including admins).
+ 'offline' => false, // none, high, low or safe.
'database' =>
array (
- 'driver' => 'mysql', // the database driver, i.e. MySQL, MariaDB...
+ 'driver' => 'mysql', // the database driver, i.e. MySQL, MariaDB, PostgreSQL, SQLite
'host' => 'localhost', // the host of the connection, localhost in most cases unless using an external service
'database' => 'flarum', // the name of the database in the instance
'username' => 'root', // database username
@@ -34,3 +34,15 @@ Here's a quick overview of what everything means with an example file:
),
);
```
+
+### Maintenance modes
+
+Flarum has a maintenance mode that can be enabled by setting the `offline` key in the `config.php` file to one of the following values:
+* `none` - No maintenance mode.
+* `high` - No one can access the forum, not even admins.
+* `low` - Only admins can access the forum.
+* `safe` - Only admins can access the forum, and no extensions are booted.
+
+This can also be configured from the admin panel's advanced settings page:
+
+![Toggle advanced page](https://user-images.githubusercontent.com/20267363/277113270-f2e9c91d-2a29-436b-827f-5c4d20e2ed54.png)
diff --git a/docs/contributing.md b/docs/contributing.md
index 4fc952fd9..8255caadc 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -4,7 +4,7 @@ Interested in contributing to Flarum development? That's great! From [opening a
Before contributing, please read the [code of conduct](code-of-conduct.md).
-This document is a guide for developers who want to contribute code to Flarum. If you're just getting started, we recommend that you read the [Getting Started](/extend/start.md) documentation in the Extension docs to understand a bit more about how Flarum works.
+This document is a guide for developers who want to contribute code to Flarum. If you're just getting started, we recommend that you read the [Getting Started](./extend/start.md) documentation in the Extension docs to understand a bit more about how Flarum works.
## Why Contribute to Flarum?
@@ -158,7 +158,7 @@ Flarum's CSS classes roughly follow the [SUIT CSS naming conventions](https://gi
### Translations
-We use a [standard key format](/extend/i18n.md#appendix-a-standard-key-format) to name translation keys descriptively and consistently.
+We use a [standard key format](./extend/i18n.md#appendix-a-standard-key-format) to name translation keys descriptively and consistently.
## Contributor License Agreement
@@ -168,4 +168,4 @@ You confirm that you are able to grant us these rights. You represent that You a
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
-The Flarum Foundation acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
\ No newline at end of file
+The Flarum Foundation acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/docs/extend/README.md b/docs/extend/README.md
index ed31d9f1a..7407779f6 100644
--- a/docs/extend/README.md
+++ b/docs/extend/README.md
@@ -30,7 +30,7 @@ If you are aiming to address a bug or shortcoming of the core, or of an existing
- [Developers explaining their workflow for extension development](https://discuss.flarum.org/d/6320-extension-developers-show-us-your-workflow)
- [Extension namespace tips](https://discuss.flarum.org/d/9625-flarum-extension-namespacing-tips)
- [Mithril js documentation](https://mithril.js.org/)
-- [Laravel API Docs](https://laravel.com/api/8.x/)
+- [Laravel API Docs](https://laravel.com/api/11.x/)
- [Flarum API Docs](https://api.flarum.org)
- [ES6 cheatsheet](https://github.com/DrkSephy/es6-cheatsheet)
diff --git a/docs/extend/admin.md b/docs/extend/admin.md
index dd0bbfad1..b7f0150f9 100644
--- a/docs/extend/admin.md
+++ b/docs/extend/admin.md
@@ -1,37 +1,31 @@
# Admin Dashboard
-Beta 15 introduced a completely redesigned admin panel and frontend API. It is now easier than ever to add settings or permissions to your extension.
+Every extension has a unique page containing information, settings, and the extension's own permissions.
-Before beta 15, extension settings were either added in a `SettingsModal` or they added a new page for more complex settings. Now, every extension has a page containing info, settings, and the extension's own permissions.
+You can register settings, permissions, or use an entirely custom page based off of the [`ExtensionPage`](https://api.docs.flarum.org/js/master/class/src/admin/components/extensionpage.js~extensionpage) component.
-You can simply register settings, extend the base [`ExtensionPage`](https://api.docs.flarum.org/js/master/class/src/admin/components/extensionpage.js~extensionpage), or provide your own completely custom page.
+## Admin Extender
-## Extension Data API
+The admin frontend allows you to add settings and permissions to your extension with very few lines of code, using the `Admin` frontend extender.
-This new API allows you to add settings to your extension with very few lines of code.
-
-### Telling the API about your extension
-
-Before you can register anything, you need to tell `ExtensionData` what extension it is about to get data for.
-
-Simply run the `for` function on `app.extensionData` passing in the id of your extension. To find you extension id, take the composer name and replace any slashes with dashes (example: 'fof/merge-discussions' becomes 'fof-merge-discussions'). Extensions with the `flarum-` and `flarum-ext-` will omit those from the name (example: 'webbinaro/flarum-calendar' becomes 'webbinaro-calendar').
-
-For the following example, we will use the fictitious extension 'acme/interstellar':
+To get started, make sure you have an `admin/extend.js` file:
```js
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
-app.initializers.add('interstellar', function(app) {
-
- app.extensionData
- .for('acme-interstellar')
-});
+export default [
+ //
+]
```
-Once that is done, you can begin adding settings and permissions.
+:::info
-:::tip Note
+Remember to export the `extend` module from your entry `admin/index.js` file:
-All registration functions on `ExtensionData` are chainable, meaning you can call them one after another without running `for` again.
+```js
+export { default as extend } from './extend';
+```
:::
@@ -39,85 +33,151 @@ All registration functions on `ExtensionData` are chainable, meaning you can cal
Adding settings fields in this way is recommended for simple items. As a rule of thumb, if you only need to store things in the settings table, this should be enough for you.
-To add a field, call the `registerSetting` function after `for` on `app.extensionData` and pass a 'setting object' as the first argument. Behind the scenes `ExtensionData` actually turns your settings into an [`ItemList`](https://api.docs.flarum.org/js/master/class/src/common/utils/itemlist.ts~itemlist), you can pass a priority number as the second argument.
+To add a field, call the `setting` method of the `Admin` extender and pass a callback that returns a 'setting object' as the first argument. Behind the scenes, the app turns your settings into an [`ItemList`](https://api.docs.flarum.org/js/master/class/src/common/utils/itemlist.ts~itemlist), you can pass a priority number as the second argument which will determine the order of the settings on the page.
Here's an example with a switch (boolean) item:
```js
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
-app.initializers.add('interstellar', function(app) {
-
- app.extensionData
- .for('acme-interstellar')
- .registerSetting(
- {
+return [
+ new Extend.Admin()
+ .setting(
+ () => ({
setting: 'acme-interstellar.coordinates', // This is the key the settings will be saved under in the settings table in the database.
- label: app.translator.trans('acme-interstellar.admin.coordinates_label'), // The label to be shown letting the admin know what the setting does.
- help: app.translator.trans('acme-interstellar.admin.coordinates_help'), // Optional help text where a longer explanation of the setting can go.
+ label: app.translator.trans('acme-interstellar.admin.coordinates_label', {}, true), // The label to be shown letting the admin know what the setting does.
+ help: app.translator.trans('acme-interstellar.admin.coordinates_help', {}, true), // Optional help text where a longer explanation of the setting can go.
type: 'boolean', // What type of setting this is, valid options are: boolean, text (or any other tag type), and select.
- },
+ }),
30 // Optional: Priority
)
-});
+];
```
If you use `type: 'select'` the setting object looks a little bit different:
```js
-{
- setting: 'acme-interstellar.fuel_type',
- label: app.translator.trans('acme-interstellar.admin.fuel_type_label'),
- type: 'select',
- options: {
- 'LOH': 'Liquid Fuel', // The key in this object is what the setting will be stored as in the database, the value is the label the admin will see (remember to use translations if they make sense in your context).
- 'RDX': 'Solid Fuel',
- },
- default: 'LOH',
-}
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
+
+return [
+ new Extend.Admin()
+ .setting(
+ () => ({
+ setting: 'acme-interstellar.fuel_type',
+ label: app.translator.trans('acme-interstellar.admin.fuel_type_label', {}, true),
+ type: 'select',
+ options: {
+ 'LOH': 'Liquid Fuel', // The key in this object is what the setting will be stored as in the database, the value is the label the admin will see (remember to use translations if they make sense in your context).
+ 'RDX': 'Solid Fuel',
+ },
+ default: 'LOH',
+ }),
+ )
+];
```
Also, note that additional items in the setting object will be used as component attrs. This can be used for placeholders, min/max restrictions, etc:
```js
-{
- setting: 'acme-interstellar.crew_count',
- label: app.translator.trans('acme-interstellar.admin.crew_count_label'),
- type: 'number',
- min: 1,
- max: 10
-}
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
+
+return [
+ new Extend.Admin()
+ .setting(
+ () => ({
+ setting: 'acme-interstellar.crew_count',
+ label: app.translator.trans('acme-interstellar.admin.crew_count_label', {}, true),
+ type: 'number',
+ min: 1,
+ max: 10
+ }),
+ )
+];
```
If you want to add something to the settings like some extra text or a more complicated input, you can also pass a callback as the first argument that returns JSX. This callback will be executed in the context of [`ExtensionPage`](https://api.docs.flarum.org/js/master/class/src/admin/components/extensionpage.js~extensionpage) and setting values will not be automatically serialized.
```js
-
-app.initializers.add('interstellar', function(app) {
-
- app.extensionData
- .for('acme-interstellar')
- .registerSetting(function () {
- if (app.session.user.username() === 'RocketMan') {
-
- return (
-
+ );
+ }
+ },
+ )
+];
+```
+
+### Available Setting Types
+
+This is a list of setting types available by default:
+
+**Toggle:** `bool` or `checkbox` or `switch` or `boolean`
+
+**Textarea:** `textarea`
+
+**Color Picker:** `color-preview`
+
+**Text Input**: `text` or any HTML input types such as `tel` or `number`
+
+```ts
+{
+ setting: 'setting_unique_key',
+ label: app.translator.trans('acme-interstellar.admin.settings.setting_unique_key', {}, true),
+ type: 'bool' // Any of the mentioned values above
+}
+```
+
+**Selection:** `select` or `dropdown` or `selectdropdown`
+
+```ts
+{
+ setting: 'setting_unique_key',
+ label: app.translator.trans('acme-interstellar.admin.settings.setting_unique_key', {}, true),
+ type: 'select', // Any of the mentioned values above
+ options: {
+ 'option_key': 'Option Label',
+ 'option_key_2': 'Option Label 2',
+ 'option_key_3': 'Option Label 3',
+ },
+ default: 'option_key'
+}
+```
+
+**Image Upload Button:** `image-upload`
+
+```ts
+{
+ setting: 'setting_unique_key',
+ label: app.translator.trans('acme-interstellar.admin.settings.setting_unique_key', {}, true),
+ type: 'image-upload',
+ name: 'my_image_name', // The name of the image, this will be used for the request to the backend.
+ routePath: '/upload-my-image', // The route to upload the image to.
+ url: () => app.forum.attribute('myImageUrl'), // The URL of the image, this will be used to preview the image.
+}
```
### Registering Permissions
-New in beta 15, permissions can now be found in 2 places. Now, you can view each extension's individual permissions on their page. All permissions can still be found on the permissions page.
+Permissions can be found in 2 places. You can view each extension's individual permissions on their dedicated page, or you can view all permissions in the main permissions page.
-In order for that to happen, permissions must be registered with `ExtensionData`. This is done in a similar way to settings, call `registerPermission`.
+In order for that to happen, permissions must be registered using the `permission` method of the `Admin` extender, similar to how settings are registered.
Arguments:
* Permission object
@@ -127,21 +187,22 @@ Arguments:
Back to our favorite rocket extension:
```js
-app.initializers.add('interstellar', function(app) {
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
- app.extensionData
- .for('acme-interstellar')
- .registerPermission(
- {
+return [
+ new Extend.Admin()
+ .permission(
+ () => ({
icon: 'fas fa-rocket', // Font-Awesome Icon
- label: app.translator.trans('acme-interstellar.admin.permissions.fly_rockets_label'), // Permission Label
+ label: app.translator.trans('acme-interstellar.admin.permissions.fly_rockets_label', {}, true), // Permission Label
permission: 'discussion.rocket_fly', // Actual permission name stored in database (and used when checking permission).
tagScoped: true, // Whether it be possible to apply this permission on tags, not just globally. Explained in the next paragraph.
- },
+ }),
'start', // Category permission will be added to on the grid
95 // Optional: Priority
- );
-});
+ )
+];
```
If your extension interacts with the [tags extension](https://github.com/flarum/tags) (which is fairly common), you might want a permission to be tag scopable (i.e. applied on the tag level, not just globally). You can do this by including a `tagScoped` attribute, as seen above. Permissions starting with `discussion.` will automatically be tag scoped unless `tagScoped: false` is indicated.
@@ -153,17 +214,23 @@ To learn more about Flarum permissions, see [the relevant docs](permissions.md).
Remember these functions can all be chained like:
```js
-app.extensionData
- .for('acme-interstellar')
- .registerSetting(...)
- .registerSetting(...)
- .registerPermission(...)
- .registerPermission(...);
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
+
+return [
+ new Extend.Admin()
+ .setting(...)
+ .permission(...)
+ .permission(...)
+ .permission(...)
+ .setting(...)
+ .setting(...)
+];
```
### Extending/Overriding the Default Page
-Sometimes you have more complicated settings that mess with relationships, or just want the page to look completely different. In this case, you will need to tell `ExtensionData` that you want to provide your own page. Note that `buildSettingComponent`, the util used to register settings by providing a descriptive object, is available as a method on `ExtensionPage` (extending from `AdminPage`, which is a generic base for all admin pages with some util methods).
+Sometimes you may have more complicated settings, or just want the page to look completely different. In this case, you will need to tell the `Admin` extender that you want to provide your own page. Note that `buildSettingComponent`, the util used to register settings by providing a descriptive object, is available as a method on `ExtensionPage` (extending from `AdminPage`, which is a generic base for all admin pages with some util methods).
Create a new class that extends the `Page` or `ExtensionPage` component:
@@ -180,35 +247,89 @@ export default class StarPage extends ExtensionPage {
```
-Then, simply run `registerPage`:
+Then, simply use the `page` method of the extender:
```js
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
import StarPage from './components/StarPage';
-app.initializers.add('interstellar', function(app) {
-
- app.extensionData
- .for('acme-interstellar')
- .registerPage(StarPage);
-});
+return [
+ new Extend.Admin()
+ .page(StarPage)
+];
```
This page will be shown instead of the default.
You can extend the [`ExtensionPage`](https://api.docs.flarum.org/js/master/class/src/admin/components/extensionpage.js~extensionpage) or extend the base `Page` and design your own!
+### Admin Search
+
+The admin dashboard has a search bar that allows you to quickly find settings and permissions. If you have used the `Admin.settings` and `Admin.permissions` extender methods, your settings and permissions will be automatically indexed and searchable. However, if you have a custom setting, or custom page that structures its content differently, then you must manually add index entries that reference your custom settings.
+
+To do this, you can use the `Admin.generalIndexItems` extender method. This method takes a callback that returns an array of index items. Each index item is an object with the following properties:
+
+```ts
+export type GeneralIndexItem = {
+ /**
+ * The unique identifier for this index item.
+ */
+ id: string;
+ /**
+ * Optional: The tree path to this item, used for grouping in the search results.
+ */
+ tree?: string[];
+ /**
+ * The label to display in the search results.
+ */
+ label: string;
+ /**
+ * Optional: The description to display in the search results.
+ */
+ help?: string;
+ /**
+ * Optional: The URL to navigate to when this item is selected.
+ * The default is to navigate to the extension page.
+ */
+ link?: string;
+ /**
+ * Optional: A callback that returns a boolean indicating whether this item should be visible in the search results.
+ */
+ visible?: () => boolean;
+};
+```
+
+Here is an example of how to add an index item:
+
+```js
+import Extend from 'flarum/common/extenders';
+import app from 'flarum/admin/app';
+
+return [
+ new Extend.Admin()
+ .generalIndexItems(() => [
+ {
+ id: 'acme-interstellar',
+ label: app.translator.trans('acme-interstellar.admin.acme_interstellar_label', {}, true),
+ help: app.translator.trans('acme-interstellar.admin.acme_interstellar_help', {}, true),
+ },
+ ])
+];
+```
+
## Composer.json Metadata
-In beta 15, extension pages make room for extra info which is pulled from extensions' composer.json.
+Extension pages make room for extra info which is pulled from extensions' composer.json.
For more information, see the [composer.json schema](https://getcomposer.org/doc/04-schema.md).
-| Description | Where in composer.json |
-| --------------------------------- | -------------------------------------- |
-| discuss.flarum.org discussion link | "forum" key inside "support" |
-| Documentation | "docs" key inside "support" |
-| Support (email) | "email" key inside "support" |
-| Website | "homepage" key |
-| Donate | "funding" key block (Note: Only the first link will be used) |
-| Source | "source" key inside "support" |
+| Description | Where in composer.json |
+|------------------------------------|--------------------------------------------------------------|
+| discuss.flarum.org discussion link | "forum" key inside "support" |
+| Documentation | "docs" key inside "support" |
+| Support (email) | "email" key inside "support" |
+| Website | "homepage" key |
+| Donate | "funding" key block (Note: Only the first link will be used) |
+| Source | "source" key inside "support" |
diff --git a/docs/extend/api.md b/docs/extend/api.md
index 0f1ad5322..e58294178 100644
--- a/docs/extend/api.md
+++ b/docs/extend/api.md
@@ -12,348 +12,1022 @@ To use the built-in REST API as part of an integration, see [Consuming the REST
Before we go into detail about how to extend Flarum's data API, it's worth thinking about the lifecycle of a typical API request:
-![Flarum API Flowchart](/en/img/api_flowchart.png)
+![Flarum API Flowchart](../assets/api_flowchart.svg)
1. An HTTP request is sent to Flarum's API. Typically, this will come from the Flarum frontend, but external programs can also interact with the API. Flarum's API mostly follows the [JSON:API](https://jsonapi.org/) specification, so accordingly, requests should follow [said specification](https://jsonapi.org/format/#fetching).
-2. The request is run through [middleware](middleware.md), and routed to the proper controller. You can learn more about controllers as a whole on our [routes and content documentation](routes.md). Assuming the request is to the API (which is the case for this section), the controller that handles the request will be a subclass of `Flarum\Api\AbstractSerializeController`.
-3. Any modifications done by extensions to the controller via the [`ApiController` extender](#extending-api-controllers) are applied. This could entail changing sort, adding includes, changing the serializer, etc.
-4. The `$this->data()` method of the controller is called, yielding some raw data that should be returned to the client. Typically, this data will take the form of a Laravel Eloquent model collection or instance, which has been retrieved from the database. That being said, the data could be anything as long as the controller's serializer can process it. Each controller is responsible for implementing its own `data` method. Note that for `PATCH`, `POST`, and `DELETE` requests, `data` will perform the operation in question, and return the modified model instance.
-5. That data is run through any pre-serialization callbacks that extensions register via the [`ApiController` extender](#extending-api-controllers).
-6. The data is passed through a [serializer](#serializers), which converts it from the backend, database-friendly format to the JSON:API format expected by the frontend. It also attaches any related objects, which are run through their own serializers. As we'll explain below, extensions can [add / override relationships and attributes](#attributes-and-relationships) at the serialization level.
+2. The request is run through [middleware](middleware.md), and routed to the proper API resource endpoint. Each API Resource is distinguished by a unique type and has a set of endpoints. You can read more about them in the below sections.
+3. Any modifications done by extensions to the API Resource endpoints via the [`ApiResource` extender](#extending-api-resources) are applied. This could entail changing sort, adding includes, eager loading relations, or executing some logic before and/or after the default implementation runs.
+4. The action of the endpoint is called, yielding some raw data that should be returned to the client. Typically, this data will take the form of a Laravel Eloquent model collection or instance, which has been retrieved from the database. That being said, the data could be anything as long as the API resource can process it. There are built-in reusable endpoint for CRUD operations, but custom endpoints can be implemented as well.
+5. Any modifications made through the [`ApiResource` extender](#extending-api-resources) to the API resource's fields will be applied. These can include adding new attributes or relationships to serialize, removing existing ones, or changing how the field value is computed.
+6. The fields (attributes and relationships) are serialized, converting the data from the backend database-friendly format to the JSON:API format expected by the frontend.
7. The serialized data is returned as a JSON response to the frontend.
8. If the request originated via the Flarum frontend's `Store`, the returned data (including any related objects) will be stored as [frontend models](#frontend-models) in the frontend store.
-## API Endpoints
+## API Resources
We learned how to use models to interact with data, but we still need to get that data from the backend to the frontend.
-We do this by writing API Controller [routes](routes.md), which implement logic for API endpoints.
+We do this by writing an API Resource for the model, which defines the fields (attributes and relationships) of the model, the endpoints of the resource API, and optionally some extra logic, such as visibility scoping, sorting options, etc. We will learn about this in the next few sections.
-As per the JSON:API convention, we'll want to add separate endpoints for each operation we support. Common operations are:
+CRUD endpoints are provided by Flarum, so you can simply add them to your API resource's `endpoints()` method. They are:
-- Listing instances of a model (possibly including searching/filtering)
-- Getting a single model instance
-- Creating a model instance
-- Updating a model instance
-- Deleting a single model instance
+- `Index`: Listing many instances of a model (possibly including searching/filtering)
+- `Show`: Getting a single model instance
+- `Create`: Creating a model instance
+- `Update`: Updating a model instance
+- `Delete`: Deleting a single model instance
-We'll go over each type of controller shortly, but once they're written, you can add these five standard endpoints (or a subset of them) using the `Routes` extender:
+:::info
-```php
- (new Extend\Routes('api'))
- ->get('/tags', 'tags.index', ListTagsController::class)
- ->get('/tags/{id}', 'tags.show', ShowTagController::class)
- ->post('/tags', 'tags.create', CreateTagController::class)
- ->patch('/tags/{id}', 'tags.update', UpdateTagController::class)
- ->delete('/tags/{id}', 'tags.delete', DeleteTagController::class)
+Flarum uses a forked version of Toby Zerner's [json-api-server](https://tobyzerner.github.io/json-api-server/). So some of what is documented there applies in Flarum, but not everything is the same.
+
+:::
+
+:::tip [Flarum CLI](https://github.com/flarum/cli)
+
+You can use the CLI to automatically create your API resource:
+```bash
+$ flarum-cli make backend api-resource
```
-:::caution
+:::
-Paths to API endpoints are not arbitrary! To support interactions with frontend models:
+***Example:*** if you had a `Label` model, the `LabelResource` you would create could look something like this:
-- The path should either be `/prefix/{id}` for get/update/delete, or `/prefix` for list/create.
-- the prefix (`tags` in the example above) must correspond to the JSON:API model type. You'll also use this model type in your serializer's `$type` attribute, and when registering the frontend model (`app.store.models.TYPE = MODEL_CLASS`).
-- The methods must match the example above.
+```php
+namespace Acme\Api;
-Also, remember that route names (`tags.index`, `tags.show`, etc) must be unique!
+use Acme\Label;
+use Flarum\Api\Context;
+use Flarum\Api\Endpoint;
+use Flarum\Api\Resource\AbstractDatabaseResource;
+use Flarum\Api\Schema;
-:::
+/** @extends AbstractDatabaseResource */
+class LabelResource extends AbstractDatabaseResource
+{
+ public function type(): string
+ {
+ return 'labels';
+ }
-The `Flarum\Api\Controller` namespace contains a number of abstract controller classes that you can extend to easily implement your JSON-API resources.
+ public function model(): string
+ {
+ return Label::class;
+ }
-:::info [Flarum CLI](https://github.com/flarum/cli)
+ public function scope(Builder $query, Context $context): void
+ {
+ $query->whereVisibleTo($context->getActor());
+ }
-You can use the CLI to automatically create your endpoint controllers:
-```bash
-$ flarum-cli make backend api-controller
+ public function endpoints(): array
+ {
+ return [
+ Endpoint\Show::make(),
+ Endpoint\Create::make()
+ ->authenticated()
+ ->can('createLabel'),
+ Endpoint\Update::make()
+ ->authenticated()
+ ->can('edit'),
+ Endpoint\Delete::make()
+ ->authenticated()
+ ->can('delete'),
+ Endpoint\Index::make()
+ ->defaultInclude(['parent']),
+ ];
+ }
+
+ /*
+ * This is only for endpoint processing and serialization.
+ * You still have to create a database migration to add the table/columns.
+ */
+ public function fields(): array
+ {
+ return [
+ Schema\Str::make('name')
+ ->requiredOnCreate()
+ ->writable(),
+ Schema\Str::make('description')
+ ->writable()
+ ->maxLength(700)
+ ->nullable(),
+ Schema\Str::make('slug')
+ ->requiredOnCreate()
+ ->writable()
+ ->unique('labels', 'slug', true)
+ ->regex('/^[^\/\\ ]*$/i'),
+ Schema\Str::make('color')
+ ->writable()
+ ->nullable()
+ ->rule('hex_color'),
+ Schema\Str::make('icon')
+ ->writable()
+ ->nullable(),
+ Schema\Boolean::make('isActive')
+ ->writable(),
+ Schema\DateTime::make('createdAt'),
+ Schema\Boolean::make('canAddToDiscussion')
+ ->get(fn (Tag $tag, FlarumContext $context) => $context->getActor()->can('addToDiscussion', $tag)),
+
+ Schema\Relationship\ToOne::make('user')
+ ->type('users')
+ ->includable(),
+ Schema\Relationship\ToOne::make('parent')
+ ->type('labels')
+ ->includable(),
+ Schema\Relationship\ToMany::make('children')
+ ->type('labels')
+ ->includable(),
+ ];
+ }
+
+ public function sorts(): array
+ {
+ return [
+ SortColumn::make('createdAt'),
+ ];
+ }
+}
```
-:::
+### Resource Definition
-### Listing Resources
+The API resource class must extend the `Flarum\Api\Resource\AbstractDatabaseResource` class when interacting with Eloquent models, and `Flarum\Api\Resource\AbstractResource` when not. The `type` method should return a unique string that identifies the resource type. In the case of a database resource, the `model` method must return the class name of the model (`::class` property).
+
+```php
+use Flarum\Api\Resource\AbstractDatabaseResource;
-For the controller that lists your resource, extend the `Flarum\Api\Controller\AbstractListController` class. At a minimum, you need to specify the `$serializer` you want to use to serialize your models, and implement a `data` method to return a collection of models. The `data` method accepts the `Request` object and the tobscure/json-api `Document`.
+class LabelResource extends AbstractDatabaseResource
+{
+ public function type(): string
+ {
+ return 'labels';
+ }
+
+ public function model(): string
+ {
+ return Label::class;
+ }
+}
+```
```php
-use Flarum\Api\Controller\AbstractListController;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Tobscure\JsonApi\Document;
+use Flarum\Api\Resource\AbstractResource;
-class ListTagsController extends AbstractListController
+class CustomResource extends AbstractResource
{
- public $serializer = TagSerializer::class;
+ public function type(): string
+ {
+ return 'custom';
+ }
- protected function data(Request $request, Document $document)
+ public function getId(object $model, Context $context): string
{
- return Tag::all();
+ return // return the model ID.
}
+
+ public function find(string $id, Context $context): ?object
+ {
+ // return the model instance.
+ }
+}
+```
+
+### Scoping Database Resources
+
+The `scope` method is used to apply a query scope to the model. This is useful for applying [visibility scoping](model-visibility.md) and ensures no data is returned that the actor should not have access to, including when the resource is a serialized relationship of another resource.
+
+```php
+use Tobyz\JsonApiServer\Context;
+use Illuminate\Database\Eloquent\Builder;
+
+public function scope(Builder $query, Context $context): void
+{
+ $query->whereVisibleTo($context->getActor());
}
```
+### Listing Resources
+
+The `Index` endpoint lists the model instances.
+
+```php
+public function endpoints(): array
+{
+ return [
+ Endpoint\Index::make(),
+ ];
+}
+```
+
+:::info
+
+Find out more about the listing endpoint in the underlying package's documentation: https://tobyzerner.github.io/json-api-server/list.html
+
+:::
+
#### Pagination
-You can allow the number of resources being **listed** to be customized by specifying the `limit` and `maxLimit` properties on your controller:
+You can paginate the resources being **listed** to by specifying the `limit` and `maxLimit` through the `paginate` method:
```php
- // The number of records included by default.
- public $limit = 20;
-
- // The maximum number of records that can be requested.
- public $maxLimit = 50;
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Index::make()
+ ->paginate(20, 50), // these are the default values, so you may omit these arguments.
+ ];
+}
```
-You can then extract pagination information from the request using the `extractLimit` and `extractOffset` methods:
+#### Sorting
+
+You can specify sort columns through the `sorts` method. For example the following will permit two sorting options: `createdAt` (in ascending order) and `-createdAt` (in descending order):
```php
-$limit = $this->extractLimit($request);
-$offset = $this->extractOffset($request);
+use Flarum\Api\Sort\SortColumn;
-return Tag::skip($offset)->take($limit);
+public function sorts(): array
+{
+ return [
+ SortColumn::make('createdAt'),
+ ];
+}
```
-To add pagination links to the JSON:API document, use the `Document::addPaginationLinks` method.
+You can specify the default sort through the `defaultSort` method on the `Index` endpoint:
-#### Sorting
+```php
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Index::make()
+ ->defaultSort('-createdAt'),
+ ->paginate(),
+ ];
+}
+```
-You can allow the sort order of resources being **listed** to be customized by specifying the `sort` and `sortField` properties on your controller:
+#### Searching and Filtering
+
+Read our [searching and filtering](search.md) guide for more information!
+
+### Showing, Creating, Updating, and Deleting Resources
+
+The `Show`, `Create`, `Update`, and `Delete` endpoints are used to get, create, update, and delete a single model instance, respectively.
+
+If your resource class extends the `AbstractDatabaseResource` class, you can directly use the endpoints.
```php
- // The default sort field and order to use.
- public $sort = ['name' => 'asc'];
-
- // The fields that are available to be sorted by.
- public $sortFields = ['firstName', 'lastName'];
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Show::make(),
+ Endpoint\Create::make(),
+ Endpoint\Update::make(),
+ Endpoint\Delete::make(),
+ ];
+}
+```
+
+If your resource class extends the `AbstractResource` class, you must implement the appropriate interfaces.
+
+```php
+use Flarum\Api\Resource\Contracts\{
+ Countable,
+ Creatable,
+ Deletable,
+ Findable,
+ Listable,
+ Paginatable,
+ Updatable
+};
+
+class CustomResource extends AbstractResource implements
+ Findable, // Show endpoint
+ Listable, // Index endpoint
+ Countable, // Optional for Index endpoints total result count
+ Paginatable, // Optional if paginating Index endpoint results
+ Creatable, // Create endpoint
+ Updatable, // Update endpoint
+ Deletable // Delete endpoint
+{
+ // ...
+}
+```
+
+:::info
+
+Find out more about these endpoint in the underlying package's documentation:
+* https://tobyzerner.github.io/json-api-server/show.html
+* https://tobyzerner.github.io/json-api-server/create.html
+* https://tobyzerner.github.io/json-api-server/update.html
+* https://tobyzerner.github.io/json-api-server/delete.html
+
+:::
+
+### Database resource hooks
+
+API database resources have additional hooks that can be used to run custom logic:
+
+```php
+public function creating(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function updating(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function saving(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function saved(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function created(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function updated(object $model, Context $context): ?object
+{
+ return $model;
+}
+
+public function deleting(object $model, Context $context): void
+{
+ //
+}
+
+public function deleted(object $model, Context $context): void
+{
+ //
+}
+
+public function mutateDataBeforeValidation(Context $context, array $data): array
+{
+ return $data;
+}
```
-You can then extract sorting information from the request using the `extractSort` method. This will return an array of sort criteria which you can apply to your query:
+## Endpoints
+
+There is a range of methods you can use to customize the behavior of your API endpoints. We will try to go through them in this section.
+
+### Authorization
+
+You can use a callback to determine whether an actor can access an endpoint. This is done through the `visible` method on the endpoint:
```php
-use Illuminate\Support\Str;
+use Flarum\Api\Context;
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Show::make()
+ ->visible(fn (Label $label, Context $context) => $context->getActor()->can('view', $label)),
+ ];
+}
+```
-// ...
+Flarum adds a couple of useful methods. The `can`, `authenticated` & `admin` methods. `can` is just the equivalent of the above example. `authenticated` checks that the actor is logged in (not a guest). `admin` checks that the actor is an admin.
-$sort = $this->extractSort($request);
-$query = Tag::query();
+```php
+use Flarum\Api\Endpoint;
-foreach ($sort as $field => $order) {
- $query->orderBy(Str::snake($field), $order);
+public function endpoints(): array
+{
+ return [
+ Endpoint\Show::make()
+ ->authenticated()
+ ->can('view'), // equivalent to $actor->can('view', $label)
+ Endpoint\Create::make()
+ ->authenticated()
+ ->can('createLabel'), // equivalent to $actor->can('createLabel'),
+ Endpoint\Update::make()
+ ->admin(), // equivalent to $actor->isAdmin()
+ ];
}
+```
+
+### Including relationship by default
-return $query->get();
+We do not recommend including relationships by default. If possible, it is better to extend the specific request payloads to be made on the frontend side and add the includes there as that keeps the API responses optimized. However, if you *really* need to include a relationship by default, you can do so through the `defaultInclude` method:
+
+```php
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Index::make()
+ ->defaultInclude(['parent']),
+ ];
+}
```
-#### Searching and Filtering
+### Lifecycle Hooks
-Read our [searching and filtering](search.md) guide for more information!
+Some methods on the endpoint allow you to hook certain logic into the lifecycle of the endpoint. These are `before`, `after`, and `beforeSerialization` which is often not very different from `after` but is called before it when available on the endpoint. For example, you can use these hooks to log additional information (such as marking notifications as read when said endpoint is accessed), or you may need to modify the resulting data before it is serialized.
+
+```php
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Index::make()
+ ->before(function (Context $context) {
+ // Do something before the endpoint logic.
+ })
+ ->after(function (Context $context, mixed $data) {
+ // Do something after the endpoint logic.
+ })
+ ->beforeSerialization(function (Context $context, mixed $results) {
+ // Do something before the data is serialized.
+ }),
+ ];
+}
+```
-### Showing a Resource
+### Eager Loading
-For the controller that shows a single resource, extend the `Flarum\Api\Controller\AbstractShowController` class. Like for the list controller, you need to specify the `$serializer` you want to use to serialize your models, and implement a `data` method to return a single model. We'll learn about serializers [in just a bit](#serializers).
+By default, relationships that are included in the API response are automatically eager loaded. However, a lot of times you will want to specify some relations to be eager loaded regardless of their inclusion in the API response. For example, if you need to access `$label->parent` to check that a field should be visible in the response, then you will need to eager load the parent relation to prevent N+1 queries.
+
+You can do this through the `eagerLoad`, `eagerLoadWhenIncluded` and `eagerLoadWhere` methods on the endpoint.
```php
-use Flarum\Api\Controller\AbstractShowController;
-use Illuminate\Support\Arr;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Tobscure\JsonApi\Document;
+use Flarum\Api\Endpoint;
+use Illuminate\Database\Eloquent\Builder;
-class ShowTagController extends AbstractShowController
+public function endpoints(): array
{
- public $serializer = TagSerializer::class;
-
- protected function data(Request $request, Document $document)
- {
- $id = Arr::get($request->getQueryParams(), 'id');
-
- return Tag::findOrFail($id);
- }
+ return [
+ Endpoint\Index::make()
+ // will always eager load the parent relation.
+ ->eagerLoad(['parent']),
+ // will eager load the parent.user relation only when parent is included in the API response.
+ ->eagerLoadWhenIncluded(['parent' => ['parent.user']])
+ // will eager load the parent relation only when the parent is active.
+ ->eagerLoadWhere('parent', function (Builder $query) {
+ $query->where('is_active', true);
+ }),
+ ];
+}
+```
+
+:::tip
+
+Use the [Clockwork](https://github.com/FriendsOfFlarum/clockwork) extension to profile your API requests and see if you are making N+1 queries. These can be very costly the larger the community is.
+
+:::
+
+### Custom Endpoints
+
+Aside from the built-in CRUD endpoints, you can also define custom endpoints. This is done through the `Endpoint\Endpoint` class. You can define the logic of the endpoint through the `action` method. Unlike the built-in CRUD endpoints, you must specify the name of the endpoint, the HTTP method, and path.
+
+If you path includes an `{id}` parameter, the model will be automatically fetched and can be accessed through `$context->model`.
+
+If the `action` method returns `null`, the API response will be an empty document. If the model is returned, it will be serialized and returned as the API response.
+
+```php
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Endpoint::make('activate')
+ ->route('POST', '/{id}/activate')
+ ->action(function (Context $context) {
+ $label = $context->model;
+ $label->isActive = true;
+ $label->save();
+
+ return $label;
+ }),
+ ];
+}
+```
+
+Alternatively you can use the `response` method to customize the response.
+
+```php
+use Flarum\Api\Endpoint;
+
+public function endpoints(): array
+{
+ return [
+ Endpoint\Endpoint::make('activate')
+ ->route('POST', '/{id}/activate')
+ ->action(function (Context $context) {
+ return ['information' => 'test'];
+ })
+ ->response(function (Context $context, array $results) {
+ // $results is the return value of the action method.
+
+ return new Response(204);
+ })
+ ];
+}
+```
+
+### Linking to endpoints internally
+
+Each resource endpoint registers a route with the name `$type.$name`. For example, the `Index` endpoint on the `LabelResource` will have a route name of `labels.index`, and the custom `activate` endpoint will have a route name of `labels.activate`. You can use the `UrlGenerator` to generate URLs to these endpoints.
+
+```php
+/** @var \Flarum\Http\UrlGenerator $url */
+$url->to('api')->route('labels.index');
+$url->to('api')->route('labels.activate', ['id' => $label->id]);
+```
+
+## Fields (Attributes and Relationships)
+
+The `fields` method on the API resource is used to define the fields (attributes and relationships) of the model. You can use the `Schema` namespace to define the various field types.
+
+The code examples below are methods within an API resource class.
+
+### Attributes
+
+Before you define an attribute, decide which type of attribute it is. The `Schema` namespace provides a range of attribute types, such as `Str`, `Integer`, `Boolean`, `DateTime` and `Arr` for arrays.
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->requiredOnCreate()
+ ->writable(),
+ Schema\Integer::make('discussionCount'),
+ Schema\Arr::make('customData'),
+ Schema\Boolean::make('isActive')
+ ->writable(),
+ Schema\DateTime::make('createdAt'),
+ ];
+}
+```
+
+### Visibility
+
+You can use the `visible` method to conditionally include an attribute in the API response.
+
+```php
+use Flarum\Api\Context;
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->visible(fn (Label $label, Context $context) => $context->getActor()->can('edit', $label)),
+ ];
+}
+```
+
+### Writability
+
+By default, a field is not writable unless you specify it to be so. You can use the `writable` method to make a field writable.
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->writable(),
+ Schema\Boolean::make('isActive')
+ // If it's only writable on create.
+ ->writableOnCreate()
+ // If it's only writable on update.
+ ->writableOnUpdate(),
+ ];
+}
+```
+
+### Requirability
+
+By default, a field is not required unless you specify it to be so. You can use the methods `required`, `requiredOnCreate`, `requiredWith`, `requiredWithout`, `requiredOnCreateWith`, `requiredOnUpdateWith`, `requiredOnCreateWithout`, `requiredOnUpdateWithout`.
+
+:::caution
+
+You will normally only want to only require fields on creation so that fields can be updated in isolation of other attributes. So we recommend using `requiredOnCreate` methods by default unless the need for otherwise arises.
+
+:::
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->requiredOnCreate(),
+ ];
}
```
-### Creating a Resource
+### Getter & Setter
-For the controller that creates a resource, extend the `Flarum\Api\Controller\AbstractCreateController` class. This is the same as the show controller, except the response status code will automatically be set to `201 Created`. You can access the incoming JSON:API document body via `$request->getParsedBody()`:
+By default, the value will be written directly to the model attribute, and read directly from the model attribute. You can use the `get` and `set` methods to customize how the value is read and written.
```php
-use Flarum\Api\Controller\AbstractCreateController;
-use Illuminate\Support\Arr;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Tobscure\JsonApi\Document;
+use Flarum\Api\Context;
+use Flarum\Api\Schema;
-class CreateTagController extends AbstractCreateController
+public function fields(): array
{
- public $serializer = TagSerializer::class;
+ return [
+ Schema\Str::make('name')
+ ->get(fn (Label $label) => strtoupper($label->name))
+ ->set(function (Label $label, string $value, Context $context) {
+ $label->name = strtolower($value);
+ }),
+ ];
+}
+```
+
+### Validation
+
+You can use the `rule` method to add a [Laravel validation rule](https://laravel.com/docs/11.x/validation#available-validation-rules) to an attribute. We've provided helper methods on some attributes for common validation rules.
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->rule('ruleName')
+ ->rule('ruleName', false) // will not apply
+ ->rule('ruleName', function (Context $context, ?Label $model) {
+ return // if the rule should apply.
+ })
+ ->rule(function (Context $context) {
+ return function ($attribute, $value, $fail) {
+ if ($value !== 'foo') {
+ $fail('The '.$attribute.' must be foo.');
+ }
+ };
+ }, $condition),
- protected function data(Request $request, Document $document)
- {
- $attributes = Arr::get($request->getParsedBody(), 'data.attributes');
-
- return Tag::create([
- 'name' => Arr::get($attributes, 'name')
- ]);
- }
+
+ Schema\Str::make('name')
+ ->requiredOnCreate() // only required when creating a new model.
+ ->maxLength(255),
+ Schema\Str::make('slug')
+ ->required() // required on both create and update.
+ ->unique('labels', 'slug', true) // unique in the labels table, ignoring the current model.
+ ->regex('/^[^\/\\ ]*$/i'), // must match the regex.
+ Schema\Str::make('color')
+ ->rule('hex_color'),
+ Schema\Number::make('price')
+ ->min(1)
+ ->max(100),
+ Schema\DateTime::make('createdAt')
+ ->before('2022-01-01')
+ ->after('2021-01-01'),
+ ];
}
```
-### Updating a Resource
+### Property
-For the controller that updates a resource, extend the `Flarum\Api\Controller\AbstractShowController` class. Like for the create controller, you can access the incoming JSON:API document body via `$request->getParsedBody()`.
+By default you should use camelCase for your attribute names and they will be automatically mapped to their snake_case equivalent when interacting with the model. But if you need to specify which property on the model the attribute should map to, you can use the `property` method.
-### Deleting a Resource
+```php
+use Flarum\Api\Schema;
-For the controller that deletes a resource, extend the `Flarum\Api\Controller\AbstractDeleteController` class. You only need to implement a `delete` method which enacts the deletion. The controller will automatically return an empty `204 No Content` response.
+public function fields(): array
+{
+ return [
+ Schema\Str::make('name')
+ ->property('name_column'),
+ ];
+}
+```
+
+### Relationship aggregates
+
+The `Number` & `Integer` attributes are able to get efficiently get relationship aggregates such as counts, sums, max, and min.
```php
-use Flarum\Api\Controller\AbstractDeleteController;
-use Illuminate\Support\Arr;
-use Psr\Http\Message\ServerRequestInterface as Request;
+use Flarum\Api\Schema\Number;
+use Flarum\Api\Schema\Integer;
-class DeleteTagController extends AbstractDeleteController
-{
- protected function delete(Request $request)
- {
- $id = Arr::get($request->getQueryParams(), 'id');
+public function fields(): array
+{
+ return [
+ Integer::make('commentCount')
+ ->countRelation('comments'),
- Tag::findOrFail($id)->delete();
+ Number::make('avgRevenue')
+ ->avgReation('reports', 'revenue'),
+
+ Number::make('revenueSum')
+ ->sumRelation('reports', 'revenue'),
+
+ Number::make('minNumber')
+ ->minRelation('posts', 'number'),
+
+ Number::make('maxNumber')
+ ->maxRelation('posts', 'number'),
+ ];
+}
+```
+
+## Relationships
+
+A relationship is a field, so all that was mentioned about fields above applies to relationships as well.
+
+There are two types of relationships: `ToOne` and `ToMany`. You can use the `Schema\Relationship\ToOne` and `Schema\Relationship\ToMany` classes to define them.
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Relationship\ToOne::make('user')
+ ->type('users'),
+ Schema\Relationship\ToMany::make('children')
+ ->type('labels'),
+ ];
+}
+```
+
+### Inclusion & Linkage
+
+You can mark a relationship as includable through the `includable` method. This means that the relationship can be included in the API response. You can also use the `withLinkage` and `withoutLinkage` methods to determine whether the relationship ID(s) should be included in the API response (`ToMany` relationships are not linked by default contrary to `ToOne` relationships).
+
+:::danger
+
+Adding linkage for `ToMany` relationships can lead to performance issues as it will include the IDs of all the related models in the API response. This is why it is not linked by default.
+
+:::
+
+```php
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Relationship\ToOne::make('user')
+ ->type('users')
+ ->includable()
+ ->withoutLinkage(),
+ ];
+}
+```
+
+:::tip
+
+Relationship linkage is the ID of the related model(s) in the API response. For example, linkage of a `ToOne` relationship would look like this:
+
+```json
+{
+ "attributes": {
+ "name": "John Doe"
+ },
+ "relationships": {
+ "user": {
+ "data": {
+ "type": "users",
+ "id": "1"
+ }
+ }
}
}
```
-### Including Relationships
+:::
+
+### Polymorphic Relationships
-To include relationships when **listing**, **showing**, or **creating** your resource, specify them in the `$include` and `$optionalInclude` properties on your controller:
+You use the `collection` method to define the resource types that a [polymorphic relationship](https://laravel.com/docs/11.x/eloquent-relationships#polymorphic-relationships) can point to.
```php
- // The relationships that are included by default.
- public $include = ['user'];
-
- // Other relationships that are available to be included.
- public $optionalInclude = ['discussions'];
+use Flarum\Api\Schema;
+
+public function fields(): array
+{
+ return [
+ Schema\Relationship\ToOne::make('subject')
+ ->collection(['users', 'discussions', 'posts']),
+ ];
+}
```
-You can then get a list of included relationships using the `extractInclude` method. This can be used to eager-load the relationships on your models before they are serialized:
+## Extending API Resources
+
+Any API Resource can be extended through the `ApiResource` extender. This is useful for adding new fields, relationships, or endpoints to an existing resource. Or when registering a new resource.
```php
-$relations = $this->extractInclude($request);
+use Flarum\Api\Resource;
+use Flarum\Api\Schema;
+use Flarum\Extend;
-return Tag::all()->load($relations);
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->fields(fn () => [
+ Schema\Str::make('customField'),
+ Schema\Relationship\ToOne::make('customRelation')
+ ->type('customRelationType'),
+ ])
+ ->endpoints(fn () => [
+ Endpoint\Endpoint::make('custom')
+ ->route('GET', '/custom')
+ ->action(fn (Context $context) => 'custom'),
+ ]),
+]
```
-### Extending API Controllers
+### Adding fields
-It is possible to customize all of these options on _existing_ API controllers too via the `ApiController` extender
+You can add fields to an existing resource through the `fields` method.
```php
-use Flarum\Api\Event\WillGetData;
-use Flarum\Api\Controller\ListDiscussionsController;
-use Illuminate\Contracts\Events\Dispatcher;
+use Flarum\Api\Resource;
+use Flarum\Api\Schema;
+use Flarum\Extend;
return [
- (new Extend\ApiController(ListDiscussionsController::class))
- ->setSerializer(MyDiscussionSerializer::class)
- ->addInclude('user')
- ->addOptionalInclude('posts')
- ->setLimit(20)
- ->setMaxLimit(50)
- ->setSort(['name' => 'asc'])
- ->addSortField('firstName')
- ->prepareDataQuery(function ($controller) {
- // Add custom logic here to modify the controller
- // before data queries are executed.
- })
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->fields(fn () => [
+ Schema\Str::make('customField'),
+ Schema\Relationship\ToOne::make('customRelation')
+ ->type('customRelationType'),
+ ]),
]
```
-The `ApiController` extender can also be used to adjust data before serialization
+### Mutating an existing field
+
+You can mutate an existing field through the `field` method. You must pass the field name as first argument.
```php
-use Flarum\Api\Event\WillSerializeData;
-use Flarum\Api\Controller\ListDiscussionsController;
-use Illuminate\Contracts\Events\Dispatcher;
+use Flarum\Api\Resource;
+use Flarum\Api\Schema;
+use Flarum\Extend;
return [
- (new Extend\ApiController(ListDiscussionsController::class))
- ->prepareDataForSerialization(function ($controller, $data, $request, $document) {
- $data->load('myCustomRelation');
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->field('email', function (Schema\Str $field) {
+ return $field->get(fn () => 'override@test');
}),
-]
+];
```
-## Serializers
+### Removing fields
+
+You can remove fields from an existing resource through the `removeField` method.
+
+```php
+use Flarum\Api\Resource;
+use Flarum\Extend;
+
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->removeField('email'),
+];
+```
-Before we can send our data to the frontend, we need to convert it to JSON:API format so that it can be consumed by the frontend.
-You should become familiar with the [JSON:API specification](https://jsonapi.org/format/).
-Flarum's JSON:API layer is powered by the [tobscure/json-api](https://github.com/tobscure/json-api) library.
+### Adding endpoints
-A serializer is just a class that converts some data (usually [Eloquent models](models.md#backend-models)) into JSON:API.
-Serializers serve as intermediaries between backend and frontend models: see the [model documentation](models.md) for more information.
-To define a new resource type, create a new serializer class extending `Flarum\Api\Serializer\AbstractSerializer`. You must specify a resource `$type` and implement the `getDefaultAttributes` method which accepts the model instance as its only argument:
+You can add endpoints to an existing resource through the `endpoints` method.
```php
-use Flarum\Api\Serializer\AbstractSerializer;
-use Flarum\Api\Serializer\UserSerializer;
+use Flarum\Api\Resource;
+use Flarum\Api\Endpoint;
+use Flarum\Extend;
-class DiscussionSerializer extends AbstractSerializer
-{
- protected $type = 'discussions';
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->endpoints(fn () => [
+ Endpoint\Show::make(),
+ Endpoint\Endpoint::make('custom')
+ ->route('GET', '/custom')
+ ->action(fn (Context $context) => 'custom'),
+ ]),
+];
+```
- protected function getDefaultAttributes($discussion)
- {
- return [
- 'title' => $discussion->title,
- ];
- }
-}
+### Mutating an existing endpoint
+
+You can mutate an existing endpoint through the `endpoint` method. You must pass either the endpoint class name or the endpoint name as first argument. You may pass an array of endpoint class names and/or names.
+
+```php
+use Flarum\Api\Resource;
+use Flarum\Api\Endpoint;
+use Flarum\Extend;
+
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->endpoint('show', function (Endpoint\Show $endpoint) {
+ return $endpoint->visible(fn (User $user, Context $context) => $context->getActor()->can('view', $user));
+ })
+ ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) {
+ return $endpoint->paginate(20, 50);
+ })
+ ->endpoint(['create', 'update'], function (Endpoint\Create|Endpoint\Update $endpoint) {
+ return $endpoint->authenticated();
+ }),
+];
```
-:::info [Flarum CLI](https://github.com/flarum/cli)
+### Removing endpoints
-You can use the CLI to automatically create your serializer:
-```bash
-$ flarum-cli make backend api-serializer
+You can remove endpoints from an existing resource through the `removeEndpoint` method.
+
+```php
+use Flarum\Api\Resource;
+use Flarum\Extend;
+
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->removeEndpoint('delete'),
+];
```
-:::
+### Adding sort columns
+
+You can add sort columns to an existing resource through the `sorts` method.
+
+```php
+use Flarum\Api\Resource;
+use Flarum\Api\Sort\SortColumn;
+use Flarum\Extend;
-### Attributes and Relationships
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->sorts(fn () => [
+ SortColumn::make('createdAt'),
+ ]),
+];
+```
-You can also specify relationships for your resource. Simply create a new method with the same name as the relation on your model, and return a call to `hasOne` or `hasMany` depending on the nature of the relationship. You must pass in the model instance and the name of the serializer to use for the related resources.
+### Mutating an existing sort column
+
+You can mutate an existing sort column through the `sort` method. You must pass the sort column name as first argument.
```php
- protected function user($discussion)
- {
- return $this->hasOne($discussion, UserSerializer::class);
- }
+use Flarum\Api\Resource;
+use Flarum\Api\Sort\SortColumn;
+use Flarum\Extend;
+
+return [
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->sort('createdAt', function (SortColumn $sort) {
+ return $sort->column('created_at');
+ }),
+];
```
-### Extending Serializers
+### Removing sort columns
-To add **attributes** and **relationships** to an existing resource type, use the `ApiSerializer` extender:
+You can remove sort columns from an existing resource through the `removeSort` method.
```php
-use Flarum\Api\Serializer\UserSerializer;
+use Flarum\Api\Resource;
+use Flarum\Extend;
return [
- (new Extend\ApiSerializer(UserSerializer::class))
- // One attribute at a time
- ->attribute('firstName', function ($serializer, $user, $attributes) {
- return $user->first_name
- })
- // Multiple modifications at once, more complex logic
- ->mutate(function($serializer, $user, $attributes) {
- $attributes['someAttribute'] = $user->someAttribute;
- if ($serializer->getActor()->can('administrate')) {
- $attributes['someDate'] = $serializer->formatDate($user->some_date);
- }
+ (new Extend\ApiResource(Resource\UserResource::class))
+ ->removeSort('createdAt'),
+];
+```
- return $attributes;
- })
- // API relationships
- ->hasOne('phone', PhoneSerializer::class)
- ->hasMany('comments', CommentSerializer::class),
+### Registering a new API Resource
+
+Simply using the `ApiResource` extender with your new resource class will register it.
+
+```php
+use Acme\Api\LabelResource;
+use Flarum\Extend;
+
+return [
+ (new Extend\ApiResource(LabelResource::class)),
]
```
-### Non-Model Serializers and `ForumSerializer`
+## Non-Model API Resources
-Serializers don't have to correspond to Eloquent models: you can define JSON:API resources for anything.
-For instance, Flarum core uses the [`Flarum\Api\Serializer\ForumSerializer`](https://api.docs.flarum.org/php/master/flarum/api/serializer/forumserializer) to send an initial payload to the frontend. This can include settings, whether the current user can perform certain actions, and other data. Many extensions add data to the payload by extending the attributes of `ForumSerializer`.
+API Resources don't have to correspond to Eloquent models: you can define JSON:API resources for anything. You need to extend the [`Flarum\Api\Rsource\AbstractResource`](https://github.com/flarum/framework/blob/2.x/framework/core/src/Api/Resource/AbstractResource.php) class instead.
+For instance, Flarum core uses the [`Flarum\Api\Resource\ForumResource`](hhttps://github.com/flarum/framework/blob/2.x/framework/core/src/Api/Resource/ForumResource.php) to send an initial payload to the frontend. This can include settings, whether the current user can perform certain actions, and other data. Many extensions add data to the payload by extending the fields of `ForumResource`.
diff --git a/docs/extend/assets.md b/docs/extend/assets.md
index e5fef4d1a..9f58662f1 100644
--- a/docs/extend/assets.md
+++ b/docs/extend/assets.md
@@ -5,4 +5,4 @@ Some extensions might want to include assets like images or JSON files in their
This is actually very easy to do. Just create an `assets` folder at the root of your extension, and place any asset files there.
Flarum will then automatically copy those files to its own `assets` directory (or other storage location if [one is offered by extensions](filesystem.md)) every time the extension is enabled or [`php flarum assets:publish`](../console.md) is executed.
-If using the default storage driver, assets will be available at `https://FORUM_URL/assets/extensions/EXTENSION_ID/file.path`. However, since other extensions might use remote filesystems, we recommend serializing the url to assets you need in the backend. See [Flarum's serialization of the logo and favicon URLs](https://github.com/flarum/framework/blob/4ecd9a9b2ff0e9ba42bb158f3f83bb3ddfc10853/framework/core/src/Api/Serializer/ForumSerializer.php#L85-L86) for an example.
+If using the default storage driver, assets will be available at `https://FORUM_URL/assets/extensions/EXTENSION_ID/file.path`. However, since other extensions might use remote filesystems, we recommend serializing the url to assets you need in the backend. See [Flarum's serialization of the logo and favicon URLs](https://github.com/flarum/framework/blob/80ded88692242e9656a1c399fa58f35f79ad9d3c/framework/core/src/Api/Resource/ForumResource.php#L104-L107) for an example.
diff --git a/docs/extend/authorization.md b/docs/extend/authorization.md
index b09fbe92b..aad1cf572 100644
--- a/docs/extend/authorization.md
+++ b/docs/extend/authorization.md
@@ -208,8 +208,8 @@ For example, if a user doesn't have permission to see search users, we shouldn't
And if a user doesn't have permission to edit users, we shouldn't show menu items for that.
Because we can't do authorization checks in the frontend, we have to perform them in the backend, and attach them to serialization of data we're sending.
-Global permissions (`viewForum`, `viewUserList`) can be included on the `ForumSerializer`, but for object-specific authorization, we may want to include those with the subject object.
+Global permissions (`viewForum`, `viewUserList`) can be included on the `ForumResource`, but for object-specific authorization, we may want to include those with the subject object.
For instance, when we return lists of discussions, we check whether the user can reply, rename, edit, and delete them, and store that data on the frontend discussion model.
It's then accessible via `discussion.canReply()` or `discussion.canEdit()`, but there's nothing magic there: it's just another attribute sent by the serializer.
-For an example of how to attach data to a serializer, see a [similar case for transmitting settings](settings.md#accessing-settings).
+For an example of how to attach data to an API resource, see a [similar case for transmitting settings](settings.md#accessing-settings).
diff --git a/docs/extend/backend-events.md b/docs/extend/backend-events.md
index 2c63f4eef..eac9f7120 100644
--- a/docs/extend/backend-events.md
+++ b/docs/extend/backend-events.md
@@ -1,6 +1,6 @@
# Backend Events
-Often, an extension will want to react to some events occuring elsewhere in Flarum. For instance, we might want to increment a counter when a new discussion is posted, send a welcome email when a user logs in for the first time, or add tags to a discussion before saving it to the database. These events are known as **domain events**, and are broadcasted across the framework through [Laravel's event system](https://laravel.com/docs/8.x/events).
+Often, an extension will want to react to some events occuring elsewhere in Flarum. For instance, we might want to increment a counter when a new discussion is posted, send a welcome email when a user logs in for the first time, or add tags to a discussion before saving it to the database. These events are known as **domain events**, and are broadcasted across the framework through [Laravel's event system](https://laravel.com/docs/11.x/events).
For a full list of backend events, see our [API documentation](https://api.docs.flarum.org/php/master/search.html?search=Event). Domain events classes are organized by namespace, usually `Flarum\TYPE\Event`.
@@ -49,7 +49,7 @@ class PostDeletedListener
}
```
-As shown above, a listener class can be used instead of a callback. This allows you to [inject dependencies](https://laravel.com/docs/8.x/container) into your listener class via constructor parameters. In this example we resolve a translator instance, but we can inject anything we want/need.
+As shown above, a listener class can be used instead of a callback. This allows you to [inject dependencies](https://laravel.com/docs/11.x/container) into your listener class via constructor parameters. In this example we resolve a translator instance, but we can inject anything we want/need.
You can also listen to multiple events at once via an event subscriber. This is useful for grouping common functionality; for instance, if you want to update some metadata on changes to posts:
diff --git a/docs/extend/code-splitting.md b/docs/extend/code-splitting.md
new file mode 100644
index 000000000..7891bcc88
--- /dev/null
+++ b/docs/extend/code-splitting.md
@@ -0,0 +1,123 @@
+# Code Splitting
+
+## Introduction
+
+Code splitting is a technique used to reduce the size of your bundle by splitting your code into various bundles which can then be loaded on demand or in parallel. This results in smaller bundles which leads to faster load time. Flarum instances can have a lot of extensions installed, and when each extension lazy loads the modules it does not immediately or frequently need, the initial load time of the forum can be significantly reduced. The opposite leads to a bloated bundle and a slow initial load time.
+
+## How to Split Your Code
+
+If you wish to split (lazy load) a module, you can use the asynchronous `import()` function. This function returns a promise which resolves to the module you are importing. Webpack will automatically split the module into a separate chunk file which will be loaded on demand.
+
+```js
+import('acme/forum/components/CustomPage').then(({ default: CustomPage }) => {
+ // do something with CustomPage
+});
+```
+
+This will create a chunk file under `js/dist/forum/components/CustomPage.js`. This chunk file will be loaded when the import is called. But before that can happen, the backend needs to be made aware of this chunk file. You do that by adding the `js/dist/forum` path as a source for the `forum` frontend. *(If the chunk was under `js/dist/admin`, you would add it as a source for the `admin` frontend, same for `js/dist/common` and `common`.)*
+
+In `extend.php`:
+
+```php
+use Flarum\Extend;
+
+return [
+ (new Extend\Frontend('forum'))
+ ->jsDirectory(__DIR__.'/js/dist/forum'),
+];
+```
+
+## Importing split modules from core or other extensions
+
+Flarum by default lazy loads certain modules of its own, such as the `LogInModal` component. If you need to import one of these modules, you can do so by just asynchronously importing it as you would any other module.
+
+```js
+import('flarum/forum/components/LogInModal').then(({ default: LogInModal }) => {
+ // do something with LogInModal
+});
+```
+
+For modules from other extensions, you can import them using the `ext:` syntax.
+
+```js
+import('ext:flarum/tags/common/components/TagSelectionModal').then(({ default: TagSelectionModal }) => {
+ // do something with CustomPage
+});
+```
+
+## Extending/Overriding/Adding split modules methods
+
+If you wish to extend, override or add a method to a split module, rather than directly accessing the module prototype `Component.prototype` or passing the prototype to `extend` or `override`, you have to pass the import path as a first argument to either `extend` or `override` utilities. The callback will be executed when the module is loaded. Checkout [Changing The UI Part 3](./frontend#changing-the-ui-part-3) for more details.
+
+## Code APIs that support lazy loading
+
+The following code APIs support lazy loading:
+
+### Async Modals
+
+You can pass a callback that returns a promise to `app.modal.show`. The modal will be shown when the promise resolves.
+
+```js
+app.modal.show(() => import('flarum/forum/components/LogInModal'));
+```
+
+### Async Pages
+
+You can pass a callback that returns a promise when declaring the page component.
+
+```js
+import Extend from 'flarum/common/extenders';
+
+export default [
+ new Extend.Routes()
+ .add('acme', '/acme', () => import('./components/CustomPage')),
+];
+```
+
+### Async Composers
+
+If you are using a custom composer like the `DiscussionComposer`, you can pass a callback that returns a promise to the `composer` method.
+
+```js
+app.composer.load(() => import('flarum/forum/components/DiscussionComposer'), { user: app.session.user }).then(() => app.composer.show());
+```
+
+### Flarum Lazy Loaded Modules
+
+You can see a list of all the modules that are lazy loaded by Flarum in the [GitHub repository](https://github.com/flarum/framework/tree/2.x/framework/core/js/dist).
+
+
+## Extending a split component class
+
+Often, you may want to create a component that extends a split component class. Here is a common example, the `fof/byobu` extension has a `PrivateDiscussionComposer` component which extends `flarum/forum/components/DiscussionComposer`.
+
+The `DiscussionComposer` along with other modules related to the composer, are lazy loaded. So this line of code will not work:
+
+```ts
+import PrivateDiscussionComposer from './discussions/PrivateDiscussionComposer';
+
+app.composer.load(PrivateDiscussionComposer, {
+ user: app.session.user,
+ recipients: recipients,
+ recipientUsers: recipients,
+});
+
+app.composer.show();
+```
+
+Because `flarum/forum/components/DiscussionComposer` is not loaded yet, the frontend will throw an error, complaining of not being able to find that module.
+
+What we need to do in this case, is to first ensure that `flarum/forum/components/DiscussionComposer` has been loaded, then we can load the custom component, that means we *have* to lazy load the custom component:
+
+```ts
+const PrivateDiscussionComposer = await app.composer
+ .load(() => import('flarum/forum/components/DiscussionComposer').then(async () => {
+ return await import('./discussions/PrivateDiscussionComposer');
+ }), {
+ user: app.session.user,
+ recipients: recipients,
+ recipientUsers: recipients,
+ });
+
+app.composer.show();
+```
diff --git a/docs/extend/console.md b/docs/extend/console.md
index 471d19681..59613387a 100644
--- a/docs/extend/console.md
+++ b/docs/extend/console.md
@@ -67,4 +67,4 @@ return [
];
```
-In the callback provided as the second argument, you can call methods on the [$event object](https://laravel.com/api/8.x/Illuminate/Console/Scheduling/Event.html) to schedule on a variety of frequencies (or apply other options, such as only running on one server). See the [Laravel documentation](https://laravel.com/docs/8.x/scheduling#scheduling-artisan-commands) for more information.
+In the callback provided as the second argument, you can call methods on the [$event object](https://laravel.com/api/11.x/Illuminate/Console/Scheduling/Event.html) to schedule on a variety of frequencies (or apply other options, such as only running on one server). See the [Laravel documentation](https://laravel.com/docs/11.x/scheduling#scheduling-artisan-commands) for more information.
diff --git a/docs/extend/database.md b/docs/extend/database.md
new file mode 100644
index 000000000..061b694ed
--- /dev/null
+++ b/docs/extend/database.md
@@ -0,0 +1,97 @@
+# Database
+
+Flarum supports a variety of database systems, including MySQL, PostgreSQL, and SQLite. Most extensions will not have to worry about the specifics of the database system, as [Laravel's query builder](https://laravel.com/docs/11.x/queries) handles the differences between them. However, you can still run into instances where you need to write certain database operations differently depending on the database system. This section aims to document some of the common pitfalls and solutions.
+
+:::warning
+
+Any usage of raw queries will require you to write the queries in a way that is compatible with all supported database systems. This is especially important if you are writing a public extension, as you cannot guarantee which database system your users will be using.
+
+:::
+
+## Specifying supported database systems
+
+You may choose to not support all database systems, but you should specify which ones you do support in your extension's `composer.json` file. This will alert users to the fact that your extension may not work with their database system.
+
+```json
+{
+ "extra": {
+ "flarum-extension": {
+ "database-support": [
+ "mysql",
+ "pgsql",
+ "sqlite"
+ ]
+ }
+ }
+}
+```
+
+## Conditional query methods
+
+Flarum adds the following query builder methods to simplify writing queries specific to a database system:
+
+```php
+// this is just an example, otherwise you would just use eloquent's whereYear method.
+$query
+ ->whenMySql(function ($query) {
+ $query->whereRaw('YEAR(created_at) = 2022');
+ })
+ ->whenPgSql(function ($query) {
+ $query->whereRaw('strftime("%Y", created_at) = 2022');
+ })
+ ->whenSqlite(function ($query) {
+ $query->whereRaw('EXTRACT(YEAR FROM created_at) = 2022');
+ });
+```
+
+## Common pitfalls
+
+### Loose data grouping
+
+In SQLite and non-strict MySQL, you can group by a column that is not in the `SELECT` clause. This fails in PostgreSQL, which requires all columns in the `SELECT` clause to be in the `GROUP BY` clause. In PostgreSQL, you can use the `DISTINCT ON` clause to achieve the same result.
+
+```php
+$query
+ ->whenPgSql(function ($query) {
+ // PostgreSQL
+ $query->select('id', 'name', 'created_at')
+ ->distinct('name')
+ ->orderBy('name');
+ }, else: function ($query) {
+ // MySQL, SQLite
+ $query->select('id', 'name', 'created_at')
+ ->groupBy('name');
+ });
+```
+
+### Seeding record with their IDs
+
+In PostgreSQL, when inserting data with the Auto increment column value specified, the database will not increase the sequence value. So you have to do it manually. Here is an example of Flarum core inserting the default member groups:
+
+```php
+ 'up' => function (Builder $schema) {
+ $db = $schema->getConnection();
+
+ $groups = [
+ [Group::ADMINISTRATOR_ID, 'Admin', 'Admins', '#B72A2A', 'fas fa-wrench'],
+ [Group::GUEST_ID, 'Guest', 'Guests', null, null],
+ [Group::MEMBER_ID, 'Member', 'Members', null, null],
+ [Group::MODERATOR_ID, 'Mod', 'Mods', '#80349E', 'fas fa-bolt']
+ ];
+
+ foreach ($groups as $group) {
+ if ($db->table('groups')->where('id', $group[0])->exists()) {
+ continue;
+ }
+
+ $db->table('groups')->insert(array_combine(['id', 'name_singular', 'name_plural', 'color', 'icon'], $group));
+ }
+
+ // PgSQL doesn't auto-increment the sequence when inserting the IDs manually.
+ if ($db->getDriverName() === 'pgsql') {
+ $table = $db->getSchemaGrammar()->wrapTable('groups');
+ $seq = $db->getSchemaGrammar()->wrapTable('groups_id_seq');
+ $db->statement("SELECT setval('$seq', (SELECT MAX(id) FROM $table))");
+ }
+ },
+```
diff --git a/docs/extend/extending-extensions.md b/docs/extend/extending-extensions.md
index f419436b6..cafd25efc 100644
--- a/docs/extend/extending-extensions.md
+++ b/docs/extend/extending-extensions.md
@@ -106,20 +106,8 @@ class SomeClass
Note that if you're importing from an optional dependency which might not be installed, you'll need to check that the class in question exists via the `class_exists` function.
-In the frontend, you can only import things that have been explicitly exported. However, first you'll need to configure your extension's webpack to allow these imports:
+In the frontend, you can import any modules exported by other extensions via the `ext:vendor/extension/.../module` syntax. For instance:
-#### webpack.config.js
-
-```js
-module.exports = require('flarum-webpack-config')({
- // Provide the extension IDs of all extensions from which your extension will be importing.
- // Do this for both full and optional dependencies.
- useExtensions: ['flarum-tags']
-});
-```
-
-Once this is done, you can import with:
-
-```js
-const allThingsExportedBySomeExtension = require('@flarum-tags');
+```ts
+import Tag from 'ext:flarum/tags/common/models/Tag';
```
diff --git a/docs/extend/extensibility.md b/docs/extend/extensibility.md
index 58a6684c8..1561ce676 100644
--- a/docs/extend/extensibility.md
+++ b/docs/extend/extensibility.md
@@ -17,13 +17,13 @@ To learn about dispatching events and defining new ones, see the [relevant docum
### Custom Extenders
-Lets say you've developed an extension that adds an alternative search driver to Flarum, but you want to allow other extensions to add support for custom filters / sorts.
+Let's say you've developed an extension that adds an alternative search driver to Flarum, but you want to allow other extensions to add support for custom filters / sorts.
A custom extender could be a good way to accomplish this.
The implementation of extenders is actually quite simple. There are 3 main steps:
1. Various methods (and the constructor) allow client code to specify options. For example:
- - Which model / API controller / validator should be extended?
+ - Which model / controller / service should be extended?
- What modifications should be made?
2. An `extend` method takes the input from step 1, and applies it by modifying various [container bindings](service-provider.md) and global static variables to achieve the desired effect. This is the "implementation" of the composer. The `extend` methods for all enabled extensions are run as part of Flarum's boot process.
3. Optionally, extenders implementing `Flarum\Extend\LifecycleInterface` can have `onEnable` and `onDisable` methods, which are run when extensions that use the extender are enabled/disabled, and are useful for tasks like clearing various caches.
diff --git a/docs/extend/filesystem.md b/docs/extend/filesystem.md
index 26149a64e..6a6c7a0e3 100644
--- a/docs/extend/filesystem.md
+++ b/docs/extend/filesystem.md
@@ -2,7 +2,7 @@
Flarum core integrates with the filesystem to store and serve assets (like compiled JS/CSS or upload logos/favicons) and avatars.
-Extensions can use Flarum's provided utils for their own filesystem interaction and file storage needs. This system is based around [Laravel's filesystem tools](https://laravel.com/docs/8.x/filesystem), which are in turn based on the [Flysystem library](https://github.com/thephpleague/flysystem).
+Extensions can use Flarum's provided utils for their own filesystem interaction and file storage needs. This system is based around [Laravel's filesystem tools](https://laravel.com/docs/11.x/filesystem), which are in turn based on the [Flysystem library](https://github.com/thephpleague/flysystem).
## Disks
@@ -11,7 +11,7 @@ Flarum core has 2 disks: `flarum-assets` and `flarum-avatars`.
### Using existing disks
-To access a disk, you'll need to retrieve it from the [Filesystem Factory](https://laravel.com/api/8.x/Illuminate/Contracts/Filesystem/Factory.html).
+To access a disk, you'll need to retrieve it from the [Filesystem Factory](https://laravel.com/api/11.x/Illuminate/Contracts/Filesystem/Factory.html).
To do so, you should inject the factory contract in your class, and access the disks you need.
Let's take a look at core's [`DeleteLogoController`](https://github.com/flarum/framework/blob/4ecd9a9b2ff0e9ba42bb158f3f83bb3ddfc10853/framework/core/src/Api/Controller/DeleteLogoController.php#L19-L58) for an example:
@@ -77,7 +77,7 @@ class DeleteLogoController extends AbstractDeleteController
}
```
-The object returned by `$filesystemFactory->disk(DISK_NAME)` implements the [Illuminate\Contracts\Filesystem\Cloud](https://laravel.com/api/8.x/Illuminate/Contracts/Filesystem/Cloud.html) interface, and can be used to create/get/move/delete files, and to get the URL to a resource.
+The object returned by `$filesystemFactory->disk(DISK_NAME)` implements the [Illuminate\Contracts\Filesystem\Cloud](https://laravel.com/api/11.x/Illuminate/Contracts/Filesystem/Cloud.html) interface, and can be used to create/get/move/delete files, and to get the URL to a resource.
### Declaring new disks
@@ -100,7 +100,7 @@ return [
Since all disks use the local filesystem by default, you'll need to provide a base path and base URL for the local filesystem.
-The config array can contain other entries supported by [Laravel disk config arrays](https://laravel.com/docs/8.x/filesystem#configuration). The `driver` key should not be provided, and will be ignored.
+The config array can contain other entries supported by [Laravel disk config arrays](https://laravel.com/docs/11.x/filesystem#configuration). The `driver` key should not be provided, and will be ignored.
## Storage drivers
diff --git a/docs/extend/forms.md b/docs/extend/forms.md
index 7cf935d17..786ac6d2d 100644
--- a/docs/extend/forms.md
+++ b/docs/extend/forms.md
@@ -17,11 +17,11 @@ You'll typically want to assign logic for reacting to input changes via Mithril'
```jsx
import Component from 'flarum/common/Component';
+import Form from 'flarum/common/Form';
import FieldSet from 'flarum/common/components/FieldSet';
import Button from 'flarum/common/components/Button';
import Switch from 'flarum/common/components/Switch';
-
class FormComponent extends Component {
oninit(vnode) {
this.textInput = "";
@@ -31,13 +31,15 @@ class FormComponent extends Component {
view() {
return (
)
}
@@ -100,6 +102,37 @@ const value = Stream();
})}>
```
+## `FormGroup` component
+
+The `FormGroup` component provides the same flexibility you get when [registering admin settings](http://localhost:3000/extend/admin#registering-settings). It allows you to pass an input type, with other information such as the label and help text, then uses the appropriate component to render the input.
+
+```jsx
+import Component from 'flarum/common/Component';
+import FormGroup from 'flarum/common/components/FormGroup';
+import Stream from 'flarum/common/utils/Stream';
+
+export default class MyComponent extends Component {
+ oninit(vnode) {
+ this.value = Stream(false);
+ }
+
+ view() {
+ return (
+
+
+
+ );
+ }
+}
+```
+
## Making Requests
In our [models](models.md) documentation, you learned how to work with models, and save model creation, changes, and deletion to the database via the Store util, which is just a wrapper around Flarum's request system, which itself is just a wrapper around [Mithril's request system](https://mithril.js.org/request.html).
diff --git a/docs/extend/frontend-pages.md b/docs/extend/frontend-pages.md
index 33ac5f472..597aebfc7 100644
--- a/docs/extend/frontend-pages.md
+++ b/docs/extend/frontend-pages.md
@@ -26,6 +26,37 @@ export default class CustomPage extends Page {
}
```
+### Forum Page Structure
+
+Flarum's forum frontend uses a generic page structure, which is defined in `flarum/common/components/PageStructure`. This structure is used by all forum pages, and is recommended for use in extensions as well. You will have noticed that each forum page has a hero, sidebar, and content area among other things. These are all defined in `PageStructure` and can be used in your extension as well.
+
+For example, a custom page component can use the `PageStructure` component as follows:
+
+```tsx
+import PageStructure from 'flarum/forum/components/PageStructure';
+
+export default class AcmePage extends Page {
+ view() {
+ return (
+ } // Optional. Extends `flarum/forum/components/Hero`
+ sidebar={() =>
+
+ );
+ }
+}
+```
+
+:::info Why use `PageStructure`?
+
+Using `PageStructure` is not required, but it is recommended. It provides a consistent structure for all pages, and allows other extensions such as themes to extend and customize pages more easily.
+
+:::
+
### Setting Page as Homepage
Flarum uses a setting to determine which page should be the homepage: this gives admins flexibility to customize their communities.
diff --git a/docs/extend/frontend.md b/docs/extend/frontend.md
index ddd7385e0..4c7a05e25 100644
--- a/docs/extend/frontend.md
+++ b/docs/extend/frontend.md
@@ -2,7 +2,7 @@
This page describes how to make changes to Flarum's user interface. How to add buttons, marquees, and blinking text. 🤩
-[Remember](/extend/start.md#architecture), Flarum's frontend is a **single-page JavaScript application**. There's no Twig, Blade, or any other kind of PHP template to speak of. The few templates that are present in the backend are only used to render search-engine-optimized content. All changes to the UI need to be made via JavaScript.
+[Remember](./start.md#architecture), Flarum's frontend is a **single-page JavaScript application**. There's no Twig, Blade, or any other kind of PHP template to speak of. The few templates that are present in the backend are only used to render search-engine-optimized content. All changes to the UI need to be made via JavaScript.
Flarum has two separate frontend applications:
@@ -52,16 +52,26 @@ js
"private": true,
"name": "@acme/flarum-hello-world",
"dependencies": {
- "flarum-webpack-config": "^1.0.0",
- "webpack": "^4.0.0",
- "webpack-cli": "^4.0.0"
- },
- "devDependencies": {
- "flarum-tsconfig": "^1.0.0"
+ "@flarum/prettier-config": "^1.0.0",
+ "flarum-tsconfig": "^2.0.0",
+ "flarum-webpack-config": "^3.0.0",
+ "prettier": "^2.5.1",
+ "typescript": "^4.5.4",
+ "typescript-coverage-report": "^0.6.1",
+ "webpack": "^5.65.0",
+ "webpack-cli": "^4.9.1"
},
"scripts": {
"dev": "webpack --mode development --watch",
- "build": "webpack --mode production"
+ "build": "webpack --mode production",
+ "analyze": "cross-env ANALYZER=true <%= params.jsPackageManager %> run build",
+ "format": "prettier --write src",
+ "format-check": "prettier --check src",
+ "clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
+ "build-typings": "<%= params.jsPackageManager %> run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && <%= params.jsPackageManager %> run post-build-typings",
+ "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
+ "check-typings": "tsc --noEmit --emitDeclarationOnly false",
+ "check-typings-coverage": "typescript-coverage-report",
}
}
```
@@ -90,16 +100,21 @@ To work properly, our extensions should use the [official flarum webpack config]
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
// and also tells your Typescript server to read core's global typings for
// access to `dayjs` and `$` in the global namespace.
- "include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"],
+ "include": [
+ "src/**/*",
+ "../vendor/*/*/js/dist-typings/@types/**/*",
+ "@types/**/*"
+ ],
"compilerOptions": {
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"baseUrl": ".",
"paths": {
- "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
+ "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"],
}
}
}
+
```
This is a standard configuration file to enable support for Typescript with the options that Flarum needs.
@@ -165,10 +180,13 @@ You should familiarize yourself with proper syntax for [importing js modules](ht
Pretty much every Flarum extension will need to import *something* from Flarum Core.
Like most extensions, core's JS source code is split up into `admin`, `common`, and `forum` folders. You can import the file by prefixing its path in the Flarum core source code with `flarum`. So `admin/components/AdminLinkButton` is available as `flarum/admin/components/AdminLinkButton`, `common/Component` is available as `flarum/common/Component`, and `forum/states/PostStreamState` is available as `flarum/forum/states/PostStreamState`.
-In some cases, an extension may want to extend code from another flarum extension. This is only possible for extensions which explicitly export their contents.
+In some cases, an extension may want to extend code from another flarum extension. You can use the same [import format](./extending-extensions#importing-from-extensions) valid for any third-party extension.
+
+For example, to import from tags extension:
-* `flarum/tags` and `flarum/flags` are currently the only bundled extensions that allow extending their JS. You can import their contents from `flarum/{EXT_NAME}/PATH` (e.g. `flarum/tags/components/TagHero`).
-* The process for extending each community extension is different; you should consult documentation for each individual extension.
+```js
+import TagsPage from 'ext:flarum/tags/components/TagsPage';
+```
### Transpilation
@@ -204,7 +222,7 @@ Flarum will make anything you `export` from `forum.js` available in the global `
:::tip External Libraries
-Only one main JavaScript file per extension is permitted. If you need to include any external JavaScript libraries, either install them with NPM and `import` them so they are compiled into your JavaScript file, or see [Routes and Content](/extend/routes.md) to learn how to add extra `