This site is built with Viewi itself. It is experimental and still in development. If you see any bugs please do not hesitate and open an issue or DM me on Twitter.

CodeIgniter 4 Integration

CodeIgniter 4

This integration is nicely done by kenjis, github.com/kenjis. Thanks a lot for contributing!

You can find the source code here github.com/kenjis/ci4-viewi-demo. And here github.com/kenjis/ci4-viewi-tour-of-heroes.

Basic concept and application flow

This example shows you how to build a basic CodeIgniter 4 application that handles all HTTP requests, and dispatches the calls through routes to your Viewi application. Viewi then renders the html response and returns it back to CodeIgniter 4 to be emitted to the web browser.

For server side rendering Viewi application will catch api calls and directly process them via your CodeIgniter 4 application.

Installation and setup

Install and prepare CodeIgniter 4

composer create-project codeigniter4/appstarter project-root

Install Viewi

composer require viewi/viewi

Create a demo app

vendor/bin/viewi new -e -f src/ViewiApp

Implementation

Clean up your public/index.php from Viewi standalone code:

<?php

// Check PHP version.
$minPhpVersion = '7.4'; // If you update this, don't forget to update `spark`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
    $message = sprintf(
        'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
        $minPhpVersion,
        PHP_VERSION
    );

    exit($message);
}

// Path to the front controller (this file)
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR);

// Ensure the current directory is pointing to the front controller's directory
chdir(FCPATH);

/*
 *---------------------------------------------------------------
 * BOOTSTRAP THE APPLICATION
 *---------------------------------------------------------------
 * This process sets up the path constants, loads and registers
 * our autoloader, along with Composer's, loads our constants
 * and fires up an environment-specific bootstrapping.
 */

// Load our paths config file
// This is the line that might need to be changed, depending on your folder structure.
require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder

$paths = new Config\Paths();

// Location of the framework bootstrap file.
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';

// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();

/*
 * ---------------------------------------------------------------
 * GRAB OUR CODEIGNITER INSTANCE
 * ---------------------------------------------------------------
 *
 * The CodeIgniter class contains the core functionality to make
 * the application run, and does all of the dirty work to get
 * the pieces all working together.
 */

$app = Config\Services::codeigniter();
$app->initialize();
$context = is_cli() ? 'php-cli' : 'web';
$app->setContext($context);

/*
 *---------------------------------------------------------------
 * LAUNCH THE APPLICATION
 *---------------------------------------------------------------
 * Now that everything is setup, it's time to actually fire
 * up the engines and make this app do its thang.
 */

$app->run();

Now you are ready to create an integration. First, you will need a new Response class for handling direct url invocations and getting raw data without modifications.

By default, Response object keeps data as a string (html or json encoded). But you need to pass an original data, for example, if your API returns a BlogPostModel, you do not want to receive a json object instead of a fully typed instance of BlogPostModel class.

Like here, your callback expects it to be PostModel:

$http->get('/api/posts/5')->then(function (PostModel $data) {
    $this->post = $data;
}, function ($error) {
    echo $error;
});

For that your new Response should preserve the original data returned from the API call. Important thing: fully typed instances are only supported on server side during SSR. Javascript doesn't support types.

<?php

namespace App\Adapters;

use CodeIgniter\HTTP\Response;

/**
 * Response class for Viewi
 */
final class RawJsonResponse extends Response
{
    /**
     * @var array|object
     */
    private $rawData;

    public function __construct()
    {
        $config = config('App');

        parent::__construct($config);
    }

    /**
     * @param array|object $data
     *
     * @return $this
     */
    public function setData($data = []): self
    {
        $this->rawData = $data;
        $this->setBody(json_encode($data));

        return $this;
    }

    /**
     * @return array|object
     */
    public function getRawData()
    {
        return $this->rawData;
    }

    /**
     * @return $this
     */
    public function withJsonHeader(): self
    {
        return $this->setHeader('Content-Type', 'application/json');
    }
}

Now you can inject a RawJsonResponse in your API handlers, like this:

<?php

namespace App\Controllers\Api;

use App\Adapters\RawJsonResponse;
use App\Controllers\BaseController;
use Components\Models\PostModel;

class Posts extends BaseController
{
    public function index($id): RawJsonResponse
    {
        $postModel          = new PostModel();
        $postModel->Id      = (int) $id;
        $postModel->Name    = 'CodeIgniter4 ft. Viewi';
        $postModel->Version = 1;

        $response = new RawJsonResponse();

        return $response->setData($postModel)->withJsonHeader();
    }
}

Now it's time to inject Viewi component into CodeIgniter 4 request processing flow. You will need a wrapper for Viewi components:

This wrapper is an action handler for CodeIgniter 4, that invokes Viewi\App with the component name and returns the content.

<?php

namespace App\Adapters;

use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Viewi\App;

class ViewiCodeIgniterComponent
{
    private string $component;
    private ?array $defaults = null;

    public function __construct(string $component, ?array $defaults = null)
    {
        $this->component = $component;
        $this->defaults  = $defaults;
    }

    public function index(array $params): ResponseInterface
    {
        $response = Services::response();

        $viewiResponse = App::run($this->component, $params);

        if (isset($this->defaults['statusCode'])) {
            $response->setStatusCode($this->defaults['statusCode']);
        }

        if (is_string($viewiResponse)) { // html
            return $response->setBody($viewiResponse);
        }

        return $response;
    }
}

After this, you are ready to create an adapter for Viewi:

<?php

namespace App\Adapters;

use CodeIgniter\CodeIgniter;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Router\RouteCollection;
use Config\Services;
use Viewi\Routing\RouteAdapterBase;

class ViewiCodeIgniterAdapter extends RouteAdapterBase
{
    private CodeIgniter $app;
    private array $nameTracker = [];
    private RouteSyntaxConverter $converter;

    public function __construct(CodeIgniter $app, RouteSyntaxConverter $converter)
    {
        $this->app       = $app;
        $this->converter = $converter;
    }

    /**
     * @param string     $method
     * @param string     $url
     * @param string     $component
     * @param array|null $defaults
     */
    public function register($method, $url, $component, $defaults): void
    {
        /** @var RouteCollection $routes */
        $routes = Services::routes();

        [$ciUrl, $paramNames] = $this->converter->convert($url);

        if (! isset($this->nameTracker[$component])) {
            $this->nameTracker[$component] = -1;
        }

        $this->nameTracker[$component]++;
        $as = ($this->nameTracker[$component] === 0) ? $component
            : "{$component}-{$this->nameTracker[$component]}";

        $routes->{$method}($ciUrl, static function (...$params) use ($component, $paramNames, $defaults) {
            $controller = new ViewiCodeIgniterComponent($component, $defaults);
            // collect params
            $viewiParams = [];

            foreach ($paramNames as $i => $name) {
                if ($i < count($params)) {
                    $viewiParams[$name] = $params[$i];
                }
            }

            return $controller->index($viewiParams);
        }, ['as' => $as]);
    }

    /**
     * @param string     $method
     * @param string     $url
     * @param array|null $params
     *
     * @return array|mixed|object
     */
    public function handle($method, $url, $params = null)
    {
        /** @var CodeIgniter $app */
        $app = Services::codeigniter(null, false);
        $app->initialize();
        $context = is_cli() ? 'php-cli' : 'web';
        $app->setContext($context);
        $app->disableFilters();

        $uri       = new URI();
        $userAgent = new UserAgent();
        $request   = new IncomingRequest(
            config('App'),
            $uri,
            'php://input',
            $userAgent
        );
        $request->setMethod($method);
        $request->setPath($url);
        $app->setRequest($request);

        $response = $app->run(null, true);

        if ($response instanceof RawJsonResponse) {
            return $response->getRawData();
        }

        return json_decode($response->getBody());
    }
}

What it does:

  • register($method, $url, $component, $defaults): void delegates routes information from Viewi to your CodeIgniter application.
  • handle($method, $url, $params = null) is being invoked during server side rendering when the component uses HttpClient to get the data from the server.
    It creates a virtual (fake) request and runs it through your CodeIgniter application as it would be any regular request from a browser.
    It's here where we need our original data without modifications:
    return $response->getRawData();

We also need a RouteSyntaxConverter to convert Viewi route syntax into CodeIgniter one:

<?php

namespace App\Adapters;

/**
 * Convert route syntax from Viewi to CodeIgniter
 */
class RouteSyntaxConverter
{
    /**
     * Viewi routes: /, *, {userId}, {userId}, {name?}, {query<[A-Za-z]+>?}
     * Replaces route params `{name}` with placeholders:
     *      {name} {name?} -> (:segment)
     *      * -> (:any)
     *
     * @phpstan-return array{0: string, 1: list<string>} [route, param_names]
     */
    public function convert(string $url): array
    {
        $ciUrl      = '';
        $paramNames = [];

        $parts = explode(
            '/',
            str_replace('*', '(:any)', trim($url, '/'))
        );

        foreach ($parts as $segment) {
            if ($segment !== '' && $segment[0] === '{') {
                $strLen    = strlen($segment) - 1;
                $regOffset = -2;
                $regex     = null;

                if ($segment[$strLen - 1] === '?') { // {optional?}
                    $strLen--;
                    $regOffset = -3;
                }

                if ($segment[$strLen - 1] === '>') { // {<regex>}
                    $strLen--;
                    $regParts = explode('<', $segment);
                    $segment  = $regParts[0];
                    // {<regex>} -> ([a-z]+), (\d+)
                    $regex = substr($regParts[1], 0, $regOffset);
                    $regex = '(' . $regex . ')';
                }

                $paramName    = substr($segment, 1, $strLen - 1);
                $paramNames[] = $paramName;
                $segment      = $regex ?? '(:segment)';
            }

            $ciUrl .= '/' . $segment;
        }

        return [$ciUrl, $paramNames];
    }
}

And now, the final step. You need to register an adapter and include your Viewi app, etc.

<?php

namespace App\Adapters;

use CodeIgniter\CodeIgniter;
use Viewi\Routing\Route;

/**
 * Viewi initializer
 */
class Viewi
{
    /**
     * Initialize Viewi
     */
    public static function init(CodeIgniter $app): void
    {
        $converter = new RouteSyntaxConverter();
        $adapter   = new ViewiCodeIgniterAdapter($app, $converter);

        Route::setAdapter($adapter);
        require __DIR__ . '/../ViewiApp/viewi.php';
    }
}

Include it after defining all routes:

<?php

namespace Config;

use App\Controllers\Api\Posts;

// Create a new instance of our RouteCollection class.
$routes = Services::routes();

// Load the system's routing file first, so that the app and ENVIRONMENT
// can override as needed.
if (is_file(SYSTEMPATH . 'Config/Routes.php')) {
    require SYSTEMPATH . 'Config/Routes.php';
}

/*
 * --------------------------------------------------------------------
 * Router Setup
 * --------------------------------------------------------------------
 */
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Home');
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps
// where controller filters or CSRF protection are bypassed.
// If you don't want to define all routes, please use the Auto Routing (Improved).
// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true.
// $routes->setAutoRoute(false);

/*
 * --------------------------------------------------------------------
 * Route Definitions
 * --------------------------------------------------------------------
 */

// We get a performance increase by specifying the default
// route since we don't have to scan directories.
// Commented out, let the Viewi handle the Home page
// $routes->get('/', 'Home::index');

$routes->get('api/posts/(:num)', [Posts::class, 'index']);

/*
 * --------------------------------------------------------------------
 * Additional Routing
 * --------------------------------------------------------------------
 *
 * There will often be times that you need additional routing and you
 * need it to be able to override any defaults in this file. Environment
 * based routes is one such time. require() additional route files here
 * to make that happen.
 *
 * You will have access to the $routes object within that file without
 * needing to reload it.
 */
if (is_file(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) {
    require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php';
}

// Viewi here, after all other routes
\App\Adapters\Viewi::init(Services::codeigniter());

And that's it. Thank you and feel free to reach out for help if you need any.