Viewi Leaf integration

You can find all files here:

github.com/ivanvoitovych/viewi-leaf

Create Leaf project

leaf create <project-name>

or

composer create-project leafs/mvc <project-name>

Install viewi

composer require viewi/viewi

Create Viewi app

Use -a parameter.

vendor/bin/viewi new -a

Run web-server

php -S localhost:8000

Run NPM watch

In another terminal.

cd viewi-app/js

npm run watch

Implementation details

Viewi routing

routes\web.php

Integrates Viewi routing.

Modify your index.php to handle requests to Viewi:


// pass action to the Viewi app
/**
 * Viewi set up
 * The idea is to let Viewi handle its own routes by registering a 404 action
 * @param RouteCollection $routes 
 * @return void 
 */
function viewiSetUp(\Leaf\App $leafApp)
{
    /**
     * @var App
     */
    $app = require __DIR__ . '/src/ViewiApp/viewi.php';
    require __DIR__ . '/src/ViewiApp/routes.php';
    $bridge = new ViewiLeafBridge($leafApp);
    $app->factory()->add(IViewiBridge::class, function () use ($bridge) {
        return $bridge;
    });
    ViewiHandler::setViewiApp($app);
    $leafApp->all('.*', function () {
        (new ViewiHandler())->handle();
    });
}

viewiSetUp($app);

$app->run();

API routes

You should use RawResponse in order to supply typed response to Viewi component.

You must use Viewi model classes defined within Viewi application, use mapping if necessary.

<?php

namespace App\Http;

use Leaf\Http\Response;

class RawResponse extends \Leaf\Http\Response
{
    /**
     * 
     * @var mixed
     */
    private $data = null;
    /**
     * 
     * @var bool Stop sending the response (no echo)
     */
    private bool $doNotSend = false;
    /**
     * Output json encoded data with an HTTP code/message
     * 
     * 
     * @return $this
     */
    public function send(): Response
    {
        if (!$this->doNotSend) parent::send();
        return $this;
    }

    public function getRawData()
    {
        return $this->data;
    }
}

// RawResponse should be injected here automatically

$app->get("/api/posts/{id}", function ($id) {
    $post = new PostModel();
    $post->Id = $id ?? 0;
    $post->Name = 'Viewi ft. Leaf';
    $post->Version = 1;
    response()->json($post);
});

Leaf bridge

src/App/Bridge/ViewiLeafBridge.php

This allows you to handle internal requests during server-side rendering (SSR).

<?php
namespace App\Bridge;

use App\Http\RawResponse;
use Viewi\Bridge\DefaultBridge;
use Viewi\Components\Http\Message\Request;
use Viewi\Engine;

class ViewiLeafBridge extends DefaultBridge
{
    public function __construct(protected \Leaf\App $leafInstance) {}

    public function request(Request $viewiRequest, Engine $engine): mixed
    {
        if ($viewiRequest->isExternal) {
            return parent::request($viewiRequest, $engine);
        }
        $originResponse = $this->leafInstance->response();
        // new response instance
        $internalResponse = new RawResponse();
        $internalResponse->makeInternal(); // make it not to send output
        // set as current response instance
        \Leaf\Config::singleton('response', function () use ($internalResponse) {
            return $internalResponse;
        });
        // handle url internally
        $this->leafInstance::handleUrl(strtoupper($viewiRequest->method), $viewiRequest->url);
        // set original response back
        \Leaf\Config::singleton('response', function () use ($originResponse) {
            return $originResponse;
        });
        // return data to Viewi
        return $internalResponse->getRawData();
    }
}

Viewi handler

Located here:

src/App/Bridge/ViewiHandler.php

<?php
namespace App\Bridge;

use Exception;
use Viewi\App;
use Viewi\Components\Http\Message\Request;
use Viewi\Router\ComponentRoute;
use Viewi\Router\Router;

class ViewiHandler
{
    protected static App $viewiApp;

    protected static Router $viewiRouter;

    public function handle()
    {
        $urlPath = explode('?', \Leaf\Http\Request::getPathInfo())[0];
        $requestMethod = \Leaf\Http\Request::getMethod();
        $match = self::$viewiRouter->resolve($urlPath, $requestMethod);
        if ($match === null) {
            throw new Exception('No route was matched!');
        }
        /** @var RouteItem */
        $routeItem = $match['item'];
        $action = $routeItem->action;
        $leafResponse = response();
        if ($action instanceof ComponentRoute) {
            $viewiRequest = new Request($urlPath, strtolower($requestMethod));
            $response = self::$viewiApp->engine()->render($action->component, $match['params'], $viewiRequest);
            if ($routeItem->transformCallback !== null && $response instanceof \Viewi\Components\Http\Message\Response) {
                $response = ($routeItem->transformCallback)($response);
            }
            foreach ($response->headers as $key => $value) {
                $leafResponse->withHeader($key, $value);
            }
            $statusCode = isset($response->headers['Location']) ? 302 : $response->status;
            $leafResponse->status($statusCode);
            $leafResponse->markup($response->body, $statusCode);
        } else {
            throw new Exception('Unknown action type.');
        }
    }

    public static function setViewiApp(App $viewiApp)
    {
        self::$viewiApp = $viewiApp;
        self::$viewiRouter = $viewiApp->router();
    }
}

Now, when you navigate to the localhost:8000, you will see your Viewi application.

Thanks and feel free to review, ask questions, contribute in any way.