Routes and Content

优质
小牛编辑
135浏览
2023-12-01

A fundamental part of extending Flarum is adding routes — both to expose new resources in the JSON-API, and to add new pages to the frontend.

Routing happens on both the PHP backend and the JavaScript frontend.

On the backend, Flarum has three collections of routes:

  • forum These routes are accessible under yourforum.com/. They include routes that show pages in the frontend (like yourforum.com/d/123-title) and other utility routes (like the reset password route).

  • admin These routes are accessible under yourforum.com/admin/. By default, there is only one admin route on the backend; the rest of the admin routing happens on the frontend.

  • api These routes are accessible under yourforum.com/api/ and make up Flarum's JSON:API.

You can add routes to any of these collections using the Routes extender. Pass the name of the collection in the extender's constructor, then call its methods to add routes.

There are methods to register routes for any HTTP request method: get, post, put, patch, and delete. All of these methods accept three arguments:

  • $path The route path using FastRoute syntax.
  • $name A unique name for the route, used for generating URLs. To avoid conflicts with other extensions, you should use your vendor name as a namespace.
  • $handler The name of the controller class that will handle the request. This will be resolved through the container.

<?php


use Flarum\Extend;
use Acme\HelloWorld\HelloWorldController;


return [
    (new Extend\Routes('forum'))
        ->get('/hello-world', 'acme.hello-world', HelloWorldController::class)
];
Flarum CLI

You can use the CLI to automatically generate your routes:


$ flarum-cli make backend route

In Flarum, Controller is just another name for a class that implements RequestHandlerInterface. Put simply, a controller must implement a handle method which receives a Request and must return a Response. Flarum includes laminas-diactoros which contains Response implementations that you can return.


<?php


namespace Acme\HelloWorld;


use Laminas\Diactoros\Response\HtmlResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;


class HelloWorldController implements RequestHandlerInterface
{
    public function handle(Request $request): Response
    {
        return new HtmlResponse('<h1>Hello, world!</h1>');
    }
}

Controllers are resolved from the container so you can inject dependencies into their constructors.

What are Controllers?

The handle method of a Controller is the code that runs when someone visits your route (or sends data to it via a form submission). Generally speaking, Controller implementations follow the pattern:

  1. Retrieve information (GET params, POST data, the current user, etc) from the Request object.
  2. Do something with that information. For instance, if our controller handles a route for creating posts, we'll want to save a new post object to the database.
  3. Return a response. Most routes will return an HTML webpage, or a JSON api response.

Sometimes you will need to capture segments of the URI within your route. You may do so by defining route parameters using the FastRoute syntax:


    (new Extend\Routes('forum'))
        ->get('/user/{id}', 'acme.user', UserController::class)

The values of these parameters will be merged with the request's query params, which you can access in your controller by calling $request->getQueryParams():


use Illuminate\Support\Arr;


$id = Arr::get($request->getQueryParams(), 'id');

You can generate URLs to any of the defined routes using the Flarum\Http\UrlGenerator class. Inject an instance of this into your controller or view, and call the to method to select a route collection. Then, you can generate a URL to a route using the name you gave it when it was defined. You can pass an array of parameters as the second argument. Parameters will fill in matching URI segments, otherwise they will be appended as query params.


$url = $this->url->to('forum')->route('acme.user', ['id' => 123, 'foo' => 'bar']);
// http://yourforum.com/user/123?foo=bar

You can inject Laravel's View factory into your controller. This will allow you to render a Blade template into your controller's response.

First, you will need to tell the view factory where it can find your extension's view files by adding a View extender to extend.php:


use Flarum\Extend;
use Illuminate\Contracts\View\Factory;


return [
    (new Extend\View)
        ->namespace('acme.hello-world', __DIR__.'/views');
];

Then, inject the factory into your controller and render your view into an HtmlResponse:


class HelloWorldController implements RequestHandlerInterface
{
    protected $view;
    
    public function __construct(Factory $view)
    {
        $this->view = $view;
    }
    
    public function handle(Request $request): Response
    {
        $view = $this->view->make('acme.hello-world::greeting');
        
        return new HtmlResponse($view->render());
    }
}

The Flarum\Api\Controller namespace contains a number of abstract controller classes that you can extend to easily implement new JSON-API resources. See Working with Data for more information.

Adding routes to the frontend actually requires you to register them on both the frontend and the backend. This is because when your route is visited, the backend needs to know to serve up the frontend, and the frontend needs to know what to display on the page.

On the backend, instead of adding your frontend route via the Routes extender, you should use the Frontend extender's route method. This always assumes GET as the method, and accepts a route path and name as the first two arguments:


    (new Extend\Frontend('forum'))
        ->route('/users', 'acme.users')

Now when yourforum.com/users is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the users route, the discussion list will still be rendered.

Flarum builds on Mithril's routing system, adding route names and an abstract class for pages (common/components/Page). To register a new route, add an object for it to app.routes:


app.routes['acme.users'] = { path: '/users', component: UsersPage };

Now when yourforum.com/users is visited, the forum frontend will be loaded and the UsersPage component will be rendered in the content area. For more information on frontend pages, please see that documentation section.

Advanced use cases might also be interested in using route resolvers.

Frontend routes also allow you to capture segments of the URI, but the Mithril route syntax is slightly different:


app.routes['acme.user'] = { path: '/user/:id', component: UserPage };

Route parameters will be passed into the attrs of the route's component. They will also be available through m.route.param

To generate a URL to a route on the frontend, use the app.route method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params.


const url = app.route('acme.user', { id: 123, foo: 'bar' });
// http://yourforum.com/users/123?foo=bar

A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a Single Page Application in the first place.

Flarum uses Mithril's routing API to provide a Link component that neatly wraps links to other internal pages. Its use is fairly simple:


import Link from 'flarum/common/components/Link';


// Link can be used just like any other component:
<Link href="/route/known/to/mithril">Hello World!</Link>


// You'll frequently use Link with generated routes:
<Link href={app.route('settings')}>Hello World!</Link>


// Link can even generate external links with the external attr:
<Link external={true} href="https://google.com">Hello World!</Link>


// The above example with external = true is equivalent to:
<a href="https://google.com">Hello World!</a>
// but is provided for flexibility: sometimes you might have links
// that are conditionally internal or external.

Whenever you visit a frontend route, the backend constructs a HTML document with the scaffolding necessary to boot up the frontend JavaScript application. You can easily modify this document to perform tasks like:

  • Changing the <title> of the page
  • Adding external JavaScript and CSS resources
  • Adding SEO content and <meta> tags
  • Adding data to the JavaScript payload (eg. to preload resources which are going to be rendered on the page immediately, thereby preventing an unnecessary request to the API)

You can make blanket changes to the frontend using the Frontend extender's content method. This accepts a closure which receives two parameters: a Flarum\Frontend\Document object which represents the HTML document that will be displayed, and the Request object.


use Flarum\Frontend\Document;
use Psr\Http\Message\ServerRequestInterface as Request;


return [
    (new Extend\Frontend('forum'))
        ->content(function (Document $document, Request $request) {
            $document->head[] = '<script>alert("Hello, world!")</script>';
        })
];

You can also add content onto your frontend route registrations:


return [
    (new Extend\Frontend('forum'))
        ->route('/users', 'acme.users', function (Document $document, Request $request) {
            $document->title = 'Users';
        })
];