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.
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.
Install and prepare CodeIgniter 4
Install Viewi
Create a demo app
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:
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.