Skip to content

Commit dd604d7

Browse files
Reorganize models and data docs (#192)
- Split it into 2 separate pages, as there's a LOT of info crammed in one article right now - Add a flowchart diagram to the API process description - A few other small updates and improvements Co-authored-by: David Wheatley <hi@davwheat.dev>
1 parent cc47487 commit dd604d7

File tree

9 files changed

+646
-567
lines changed

9 files changed

+646
-567
lines changed

docs/.vuepress/config/locales/en/sidebar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ module.exports = {
88
'start',
99
'frontend',
1010
'routes',
11-
'data',
11+
'models',
12+
'api',
1213
'distribution'
1314
]
1415
},
132 KB
Loading

docs/extend/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ If you are aiming to address a bug or shortcoming of the core, or of an existing
2929
- [Developers explaining their workflow for extension development](https://discuss.flarum.org/d/6320-extension-developers-show-us-your-workflow)
3030
- [Extension namespace tips](https://discuss.flarum.org/d/9625-flarum-extension-namespacing-tips)
3131
- [Mithril js documentation](https://mithril.js.org/)
32-
- [Laravel API Docs](https://laravel.com/api/6.x/)
32+
- [Laravel API Docs](https://laravel.com/api/8.x/)
3333
- [Flarum API Docs](https://api.flarum.org)
3434
- [ES6 cheatsheet](https://github.com/DrkSephy/es6-cheatsheet)
3535
- [Flarum Blank Extension Generator](https://discuss.flarum.org/d/11333-flarum-extension-generator-by-reflar/)

docs/extend/api.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# API and Data Flow
2+
3+
In the [previous article](models.md), we learned how Flarum uses models to interact with data. Here, we'll learn how to get that data from the database to the JSON-API to the frontend, and all the way back again.
4+
5+
## API Request Lifecycle
6+
7+
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:
8+
9+
![Flarum API Flowchart](/en/api_flowchart.png)
10+
11+
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).
12+
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`.
13+
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.
14+
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.
15+
5. That data is run through any pre-serialization callbacks that extensions register via the [`ApiController` extender](#extending-api-controllers).
16+
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.
17+
7. The serialized data is returned as a JSON response to the frontend.
18+
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.
19+
20+
## API Endpoints
21+
22+
We learned how to use models to interact with data, but we still need to get that data from the backend to the frontend.
23+
We do this by writing API Controller [routes](routes.md), which implement logic for API endpoints.
24+
25+
As per the JSON:API convention, we'll want to add separate endpoints for each operation we support. Common operations are:
26+
27+
- Listing instances of a model (possibly including searching/filtering)
28+
- Getting a single model instance
29+
- Creating a model instance
30+
- Updating a model instance
31+
- Deleting a single model instance
32+
33+
We'll go over each type of controler shortly, but once they're written, you can add these five standard endpoints (or a subset of them) using the `Routes` extender:
34+
35+
```php
36+
(new Extend\Routes('api'))
37+
->get('/tags', 'tags.index', ListTagsController::class)
38+
->get('/tags/{id}', 'tags.show', ShowTagController::class)
39+
->post('/tags', 'tags.create', CreateTagController::class)
40+
->patch('/tags/{id}', 'tags.update', UpdateTagController::class)
41+
->delete('/tags/{id}', 'tags.delete', DeleteTagController::class)
42+
```
43+
44+
::: warning
45+
Paths to API endpoints are not arbitrary! To support interactions with frontend models:
46+
47+
- The path should either be `/prefix/{id}` for get/update/delete, or `/prefix` for list/create.
48+
- 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`).
49+
- The methods must match the example above.
50+
51+
Also, remember that route names (`tags.index`, `tags.show`, etc) must be unique!
52+
:::
53+
54+
The `Flarum\Api\Controller` namespace contains a number of abstract controller classes that you can extend to easily implement your JSON-API resources.
55+
56+
### Listing Resources
57+
58+
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`.
59+
60+
```php
61+
use Flarum\Api\Controller\AbstractListController;
62+
use Psr\Http\Message\ServerRequestInterface as Request;
63+
use Tobscure\JsonApi\Document;
64+
65+
class ListTagsController extends AbstractListController
66+
{
67+
public $serializer = TagSerializer::class;
68+
69+
protected function data(Request $request, Document $document)
70+
{
71+
return Tag::all();
72+
}
73+
}
74+
```
75+
76+
#### Pagination
77+
78+
You can allow the number of resources being **listed** to be customized by specifying the `limit` and `maxLimit` properties on your controller:
79+
80+
```php
81+
// The number of records included by default.
82+
public $limit = 20;
83+
84+
// The maximum number of records that can be requested.
85+
public $maxLimit = 50;
86+
```
87+
88+
You can then extract pagination information from the request using the `extractLimit` and `extractOffset` methods:
89+
90+
```php
91+
$limit = $this->extractLimit($request);
92+
$offset = $this->extractOffset($request);
93+
94+
return Tag::skip($offset)->take($limit);
95+
```
96+
97+
To add pagination links to the JSON:API document, use the `Document::addPaginationLinks` method.
98+
99+
#### Sorting
100+
101+
You can allow the sort order of resources being **listed** to be customized by specifying the `sort` and `sortField` properties on your controller:
102+
103+
```php
104+
// The default sort field and order to use.
105+
public $sort = ['name' => 'asc'];
106+
107+
// The fields that are available to be sorted by.
108+
public $sortFields = ['firstName', 'lastName'];
109+
```
110+
111+
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:
112+
113+
```php
114+
$sort = $this->extractSort($request);
115+
$query = Tag::query();
116+
117+
foreach ($sort as $field => $order) {
118+
$query->orderBy(snake_case($field), $order);
119+
}
120+
121+
return $query->get();
122+
```
123+
124+
#### Searching and Filtering
125+
126+
Read our [searching and filtering](search.md) guide for more information!
127+
128+
### Showing a Resource
129+
130+
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).
131+
132+
```php
133+
use Flarum\Api\Controller\AbstractShowController;
134+
use Illuminate\Support\Arr;
135+
use Psr\Http\Message\ServerRequestInterface as Request;
136+
use Tobscure\JsonApi\Document;
137+
138+
class ShowTagController extends AbstractShowController
139+
{
140+
public $serializer = TagSerializer::class;
141+
142+
protected function data(Request $request, Document $document)
143+
{
144+
$id = Arr::get($request->getQueryParams(), 'id');
145+
146+
return Tag::findOrFail($id);
147+
}
148+
}
149+
```
150+
151+
### Creating a Resource
152+
153+
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()`:
154+
155+
```php
156+
use Flarum\Api\Controller\AbstractCreateController;
157+
use Illuminate\Support\Arr;
158+
use Psr\Http\Message\ServerRequestInterface as Request;
159+
use Tobscure\JsonApi\Document;
160+
161+
class CreateTagController extends AbstractCreateController
162+
{
163+
public $serializer = TagSerializer::class;
164+
165+
protected function data(Request $request, Document $document)
166+
{
167+
$attributes = Arr::get($request->getParsedBody(), 'data.attributes');
168+
169+
return Tag::create([
170+
'name' => Arr::get($attributes, 'name')
171+
]);
172+
}
173+
}
174+
```
175+
176+
### Updating a Resource
177+
178+
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()`.
179+
180+
### Deleting a Resource
181+
182+
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.
183+
184+
```php
185+
use Flarum\Api\Controller\AbstractDeleteController;
186+
use Illuminate\Support\Arr;
187+
use Psr\Http\Message\ServerRequestInterface as Request;
188+
189+
class DeleteTagController extends AbstractDeleteController
190+
{
191+
protected function delete(Request $request)
192+
{
193+
$id = Arr::get($request->getQueryParams(), 'id');
194+
195+
Tag::findOrFail($id)->delete();
196+
}
197+
}
198+
```
199+
200+
### Including Relationships
201+
202+
To include relationships when **listing**, **showing**, or **creating** your resource, specify them in the `$include` and `$optionalInclude` properties on your controller:
203+
204+
```php
205+
// The relationships that are included by default.
206+
public $include = ['user'];
207+
208+
// Other relationships that are available to be included.
209+
public $optionalInclude = ['discussions'];
210+
```
211+
212+
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:
213+
214+
```php
215+
$relations = $this->extractInclude($request);
216+
217+
return Tag::all()->load($relations);
218+
```
219+
220+
### Extending API Controllers
221+
222+
It is possible to customize all of these options on _existing_ API controllers too via the `ApiController` extender
223+
224+
```php
225+
use Flarum\Api\Event\WillGetData;
226+
use Flarum\Api\Controller\ListDiscussionsController;
227+
use Illuminate\Contracts\Events\Dispatcher;
228+
229+
return [
230+
(new Extend\ApiController(ListDiscussionsController::class))
231+
->setSerializer(MyDiscussionSerializer::class)
232+
->addInclude('user')
233+
->addOptionalInclude('posts')
234+
->setLimit(20)
235+
->setMaxLimit(50)
236+
->setSort(['name' => 'asc'])
237+
->addSortField('firstName')
238+
->prepareDataQuery(function ($controller) {
239+
// Add custom logic here to modify the controller
240+
// before data queries are executed.
241+
})
242+
]
243+
```
244+
245+
The `ApiController` extender can also be used to adjust data before serialization
246+
247+
```php
248+
use Flarum\Api\Event\WillSerializeData;
249+
use Flarum\Api\Controller\ListDiscussionsController;
250+
use Illuminate\Contracts\Events\Dispatcher;
251+
252+
return [
253+
(new Extend\ApiController(ListDiscussionsController::class))
254+
->prepareDataForSerialization(function ($controller, $data, $request, $document) {
255+
$data->load('myCustomRelation');
256+
}),
257+
]
258+
```
259+
260+
## Serializers
261+
262+
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.
263+
You should become familiar with the [JSON:API specification](https://jsonapi.org/format/).
264+
Flarum's JSON:API layer is powered by the [tobscure/json-api](https://github.com/tobscure/json-api) library.
265+
266+
A serializer is just a class that converts some data (usually [Eloquent models](models.md#backend-models)) into JSON:API.
267+
Serializers serve as intermediaries between backend and frontend models: see the [model documentation](models.md) for more information.
268+
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:
269+
270+
```php
271+
use Flarum\Api\Serializer\AbstractSerializer;
272+
use Flarum\Api\Serializer\UserSerializer;
273+
274+
class DiscussionSerializer extends AbstractSerializer
275+
{
276+
protected $type = 'discussions';
277+
278+
protected function getDefaultAttributes($discussion)
279+
{
280+
return [
281+
'title' => $discussion->title,
282+
];
283+
}
284+
}
285+
```
286+
287+
### Attributes and Relationships
288+
289+
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.
290+
291+
```php
292+
protected function user($discussion)
293+
{
294+
return $this->hasOne($discussion, UserSerializer::class);
295+
}
296+
```
297+
298+
### Extending Serializers
299+
300+
To add **attributes** and **relationships** to an existing resource type, use the `ApiSerializer` extender:
301+
302+
```php
303+
use Flarum\Api\Serializer\UserSerializer;
304+
305+
return [
306+
(new Extend\ApiSerializer(UserSerializer::class))
307+
// One attribute at a time
308+
->attribute('firstName', function ($serializer, $user, $attributes) {
309+
return $user->first_name
310+
})
311+
// Multiple modifications at once, more complex logic
312+
->mutate(function($serializer, $user, $attributes) {
313+
$attributes['someAttribute'] = $user->someAttribute;
314+
if ($serializer->getActor()->can('administrate')) {
315+
$attributes['someDate'] = $serializer->formatDate($user->some_date);
316+
}
317+
318+
return $attributes;
319+
})
320+
// API relationships
321+
->hasOne('phone', PhoneSerializer::class)
322+
->hasMany('comments', CommentSerializer::class),
323+
]
324+
```
325+
326+
### Non-Model Serializers and `ForumSerializer`
327+
328+
Serializers don't have to correspond to Eloquent models: you can define JSON:API resources for anything.
329+
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`.

0 commit comments

Comments
 (0)