Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: 2.0 docs #466

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion docs/extend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
325 changes: 223 additions & 102 deletions docs/extend/admin.md

Large diffs are not rendered by default.

1,080 changes: 877 additions & 203 deletions docs/extend/api.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/extend/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions docs/extend/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
4 changes: 2 additions & 2 deletions docs/extend/backend-events.md
Original file line number Diff line number Diff line change
@@ -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`.

Expand Down Expand Up @@ -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:

Expand Down
87 changes: 87 additions & 0 deletions docs/extend/code-splitting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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 split modules

If you wish to extend a split module, rather than passing the prototype to `extend` or `override`, you can pass the import path as a first argument. 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).
2 changes: 1 addition & 1 deletion docs/extend/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
97 changes: 97 additions & 0 deletions docs/extend/database.md
Original file line number Diff line number Diff line change
@@ -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))");
}
},
```
18 changes: 3 additions & 15 deletions docs/extend/extending-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
```
4 changes: 2 additions & 2 deletions docs/extend/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions docs/extend/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading