API and Data Flow
In the previous article, 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.
To use the built-in REST API as part of an integration, see Consuming the REST API.
API Request Lifecycle
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:
- 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 specification, so accordingly, requests should follow said specification.
- The request is run through middleware, 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.
- Any modifications done by extensions to the API Resource endpoints via the
ApiResourceextender are applied. This could entail changing sort, adding includes, eager loading relations, or executing some logic before and/or after the default implementation runs. - 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.
- Any modifications made through the
ApiResourceextender 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. - 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.
- The serialized data is returned as a JSON response to the frontend.
- If the request originated via the Flarum frontend's
Store, the returned data (including any related objects) will be stored as frontend models in the frontend store.
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 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.
CRUD endpoints are provided by Flarum, so you can simply add them to your API resource's endpoints() method. They are:
Index: Listing many instances of a model (possibly including searching/filtering)Show: Getting a single model instanceCreate: Creating a model instanceUpdate: Updating a model instanceDelete: Deleting a single model instance
Flarum uses a forked version of Toby Zerner's json-api-server. So some of what is documented there applies in Flarum, but not everything is the same.
You can use the CLI to automatically create your API resource:
$ flarum-cli make backend api-resource
Example: if you had a Label model, the LabelResource you would create could look something like this:
namespace Acme\Api;
use Acme\Label;
use Flarum\Api\Context;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Api\Schema;
/** @extends AbstractDatabaseResource<Label> */
class LabelResource extends AbstractDatabaseResource
{
public function type(): string
{
return 'labels';
}
public function model(): string
{
return Label::class;
}
public function scope(Builder $query, Context $context): void
{
$query->whereVisibleTo($context->getActor());
}
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
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).
use Flarum\Api\Resource\AbstractDatabaseResource;
class LabelResource extends AbstractDatabaseResource
{
public function type(): string
{
return 'labels';
}
public function model(): string
{
return Label::class;
}
}
use Flarum\Api\Resource\AbstractResource;
class CustomResource extends AbstractResource
{
public function type(): string
{
return 'custom';
}
public function getId(object $model, Context $context): string
{
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 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.
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.
public function endpoints(): array
{
return [
Endpoint\Index::make(),
];
}
Find out more about the listing endpoint in the underlying package's documentation: https://tobyzerner.github.io/json-api-server/list.html