Guards and Middleware

Viewi provides you a mechanism to run additional code logic before navigating to a certain route. You can either allow a navigation or cancel it.

Implementation

To implement a middleware simply create a class that implements Viewi\Components\Middleware\IMIddleware interface:

interface IMIddleware
{
    function run(IMIddlewareContext $c);
}

Where IMIddlewareContext is:

interface IMIddlewareContext
{
    function next(bool $allow = true);
}

For example:

<?php

namespace Components\Services\Middleware;

use Viewi\Components\Middleware\IMIddleware;
use Viewi\Components\Middleware\IMIddlewareContext;
use Viewi\Components\Routing\ClientRoute;
use Viewi\DI\Singleton;

#[Singleton]
class MemberGuard implements IMIddleware
{
    public function __construct(private ClientRoute $route)
    {
    }

    public function run(IMIddlewareContext $c)
    {
        $c->next();
        // OR cancel the navigation and redirect
        $c->next(false); // cancel
        $this->route->navigate('/'); // redirect
    }
}

Using a middleware

Now you can use it with by setting a Viewi\Components\Attributes\Middleware attribute for your component and providing a list of middleware classes:

#[Middleware([MemberGuard::class])]

<?php

namespace Components\Views\Pages;

use Components\Services\Middleware\MemberGuard;
use Viewi\Components\Attributes\Middleware;
use Viewi\Components\BaseComponent;

#[Middleware([MemberGuard::class])]
class MemberPage extends BaseComponent
{
}
$router->get('/member', MemberPage::class);

Now every time you navigate to this component the additional logic will be executed before making the actual navigation.

You can chain as many middleware guards as you want.

Guard parameters

To pass values to a guard, write the entry as an array: the guard class first, then the arguments.

#[Middleware([[HasPermission::class, 'EditPosts']])]
class EditPostPage extends BaseComponent
{
}

The arguments are matched to the guard's constructor parameters by position, and injected on both the server and the browser. Services are still injected as usual:

<?php

namespace Components\Services\Middleware;

use Components\Services\SessionService;
use Viewi\Components\Middleware\IMIddleware;
use Viewi\Components\Middleware\IMIddlewareContext;
use Viewi\Components\Routing\ClientRoute;

class HasPermission implements IMIddleware
{
    public function __construct(
        private string $permission,
        private SessionService $session,
        private ClientRoute $route
    ) {
    }

    public function run(IMIddlewareContext $c)
    {
        if ($this->session->can($this->permission)) {
            $c->next();
            return;
        }
        $c->next(false);
        $this->route->navigate('/');
    }
}

This way one guard class can serve several pages that differ only by a value.

You can mix both forms in one list: #[Middleware([AuthGuard::class, [HasPermission::class, 'EditPosts']])].

Please note: a guard that takes arguments must not be #[Singleton] or #[Scoped]. Those are cached by class name, so every page would get the guard created with the first page's arguments.

Route parameters in a guard

A guard receives the route parameters the same way: a constructor parameter named like a route parameter gets its value.

$router->get('/post/{id}/edit', EditPostPage::class);
class CanEditPost implements IMIddleware
{
    public function __construct(private int $id, private PostService $posts)
    {
    }

    public function run(IMIddlewareContext $c)
    {
        $c->next($this->posts->canEdit($this->id));
    }
}

If a guard argument has the same name as a route parameter, the guard argument wins.