linkfast/exengine

API-first Microframework for PHP 8.1+

Maintainers

Package info

gitlab.com/linkfast/oss/exengine

Issues

Documentation

pkg:composer/linkfast/exengine

Statistics

Installs: 9

Dependents: 0

Suggesters: 1

Stars: 2

1.2.0 2026-03-29 00:39 UTC

This package is auto-updated.

Last update: 2026-03-29 00:41:12 UTC


README

A lightweight, modern PHP microframework for building JSON APIs

ExEngine is a high-performance, API-first framework for PHP 8.1+ designed for rapid development of RESTful APIs and microservices. Built with dependency injection, filters, and automatic routing at its core.

© LinkFast S.A. (https://linkfast.io)

PHP Version License

Features

  • 🚀 Zero Configuration - Get started with minimal setup
  • 🎯 Auto-Routing - Convention-based URL routing to controllers
  • 💉 Dependency Injection - Built-in DI container for components
  • 🔄 Request/Response Filters - Middleware-style request lifecycle hooks
  • 📦 REST Controllers - First-class support for RESTful APIs
  • 🪵 Flexible Logging - Multiple logger implementations (JSON, ErrorLog, Monolog)
  • 📊 Structured Responses - Automatic JSON serialization with standard response format
  • 🎨 DataObjects - Type-safe data serialization with null suppression
  • Production Mode - Optimized performance with disabled runtime checks
  • 🔧 Highly Extensible - Component-based architecture

Requirements

  • PHP 8.1 or higher
  • ext-json

Installation

Via Composer (Recommended)

composer require linkfast/exengine

Manual Installation

Download the latest release and include the bootstrap file:

require 'exengine/src/bootstrap.php';

Quick Start

1. Create Your Launcher

Create an api.php file in your web-accessible directory:

<?php
require 'vendor/autoload.php';

use ExEngine\ExEngine;

// Default setup with default configuration
new ExEngine(__DIR__);

2. Create a Controller

Create App/Hello.php:

<?php
namespace App;

use ExEngine\Controller;

class Hello extends Controller 
{
    public function world(): array 
    {
        return [
            "message" => "Hello, World!",
            "timestamp" => time()
        ];
    }
}

3. Test It

Navigate to: http://localhost/api.php/Hello/world

Response:

{
    "took": 0,
    "code": 200,
    "data": {
        "message": "Hello, World!",
        "timestamp": 1711526280
    },
    "error": false
}

Core Concepts

Controllers

ExEngine supports two types of controllers:

Standard Controllers

Standard controllers extend ExEngine\Controller and use method-based routing:

<?php
namespace App;

use ExEngine\Controller;

class User extends Controller 
{
    public function profile(string $userId): array 
    {
        return [
            "userId" => $userId,
            "name" => "John Doe"
        ];
    }
    
    public function settings(): array 
    {
        return ["theme" => "dark"];
    }
}

URLs:

  • GET /api.php/User/profile/123profile("123")
  • GET /api.php/User/settingssettings()

REST Controllers

REST controllers extend ExEngine\RestController and map HTTP methods automatically:

<?php
namespace App;

use ExEngine\RestController;

class Article extends RestController 
{
    public function get(?array $arguments): array 
    {
        // GET /api.php/Article/123
        $id = $arguments[0] ?? null;
        return ["id" => $id, "title" => "Article Title"];
    }
    
    public function post(?array $arguments): array 
    {
        // POST /api.php/Article
        $title = $_POST['title'];
        return [
            "__useStandardResponse" => true,
            "result" => "created",
            "id" => 456
        ];
    }
    
    public function put(?array $arguments): array 
    {
        // PUT /api.php/Article/123
        return ["result" => "updated"];
    }
    
    public function delete(?array $arguments): array 
    {
        // DELETE /api.php/Article/123
        return ["result" => "deleted"];
    }
}

Configuration

ExEngine provides flexible configuration through the Config class:

MinimalConfig (Recommended)

For quick setup with essential configuration:

<?php
require 'vendor/autoload.php';

use ExEngine\ExEngine;
use ExEngine\Config\MinimalConfig;
use ExEngine\Config\Routing;
use ExEngine\Enum\Config\RoutingType;
use ExEngine\Logger\ErrorLogLogger;

const EXENGINE_LOG = true;
const EXENGINE_LOG_WEB = true;

new ExEngine(
    new MinimalConfig(
        __DIR__,
        new Routing(RoutingType::CONTROLLER, 'Home/index'),
        ErrorLogLogger::class
    )
);

Custom Configuration

Extend the Config class for full control:

<?php
namespace App;

use ExEngine\Config;
use ExEngine\Config\Routing;
use ExEngine\Enum\Config\RoutingType;

class AppConfig extends Config 
{
    public function __construct(string $launcherPath) 
    {
        parent::__construct($launcherPath);
        
        // Set namespace for controllers
        $this->_namespace = 'App';
        
        // Set controllers location
        $this->_controllersLocation = 'Controllers';
        
        // Enable production mode
        $this->_production = false;
        
        // Configure default routing
        $this->defaultRouting(
            new Routing(RoutingType::CONTROLLER, 'Home/index')
        );
        
        // Register components
        $this->component(Database::class);
        $this->component(new Cache("redis://localhost"));
        
        // Register filters
        $this->requestFilter(AuthFilter::class);
        $this->responseFilter(CorsFilter::class);
    }
}

Dependency Injection

ExEngine features a powerful dependency injection system for components:

Registering Components

<?php
class AppConfig extends Config 
{
    public function __construct(string $launcherPath) 
    {
        parent::__construct($launcherPath);
        
        // Register by class name (new instance per injection)
        $this->component(DatabaseService::class);
        
        // Register by instance (singleton)
        $this->component(new CacheService("redis://localhost"));
        
        // Register with custom name
        $this->component(LogService::class, 'customLogger');
    }
}

Using Components

Components are automatically injected into controller constructors:

<?php
namespace App;

use ExEngine\Controller;

class User extends Controller 
{
    private DatabaseService $db;
    private CacheService $cache;
    
    public function __construct(
        DatabaseService $db,
        CacheService $cache
    ) {
        $this->db = $db;
        $this->cache = $cache;
    }
    
    public function list(): array 
    {
        $cached = $this->cache->get('users');
        if ($cached) return $cached;
        
        $users = $this->db->query("SELECT * FROM users");
        $this->cache->set('users', $users);
        return $users;
    }
}

Built-in Injectable Types

ExEngine automatically injects these types:

  • ExEngine\ExEngine - The framework instance
  • ExEngine\Logger - Logger instance
  • ExEngine\Http\Request - HTTP request wrapper
<?php
use ExEngine\Controller;
use ExEngine\Logger;
use ExEngine\Http\Request;

class MyController extends Controller 
{
    public function __construct(
        Logger $logger,
        Request $request
    ) {
        $logger->info("Controller instantiated");
        $logger->debug("Request URI", ["uri" => $request->uri()]);
    }
}

Filters

Filters provide middleware-style request/response processing:

Request Filters

Execute before controller methods:

<?php
namespace App\Filters;

use ExEngine\RequestFilter;
use ExEngine\FilterException;

class AuthFilter extends RequestFilter 
{
    public function execute(array &$filterData): mixed 
    {
        $token = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
        
        if (!$token) {
            throw new FilterException("Unauthorized", 401);
        }
        
        // Validate token and store user data
        $user = $this->validateToken($token);
        $filterData['user'] = $user;
        
        return $user;
    }
    
    private function validateToken(string $token): array 
    {
        // Token validation logic
        return ["id" => 1, "name" => "John"];
    }
}

Response Filters

Execute after controller methods:

<?php
namespace App\Filters;

use ExEngine\ResponseFilter;

class CorsFilter extends ResponseFilter 
{
    public function execute(mixed $controllerResponse): mixed 
    {
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
        header('Access-Control-Allow-Headers: Content-Type, Authorization');
        
        return $controllerResponse;
    }
}

Registering Filters

<?php
class AppConfig extends Config 
{
    public function __construct(string $launcherPath) 
    {
        parent::__construct($launcherPath);
        
        // Request filters execute in order
        $this->requestFilter(AuthFilter::class);
        $this->requestFilter(ValidationFilter::class);
        
        // Response filters execute in order
        $this->responseFilter(CorsFilter::class);
        $this->responseFilter(CompressionFilter::class);
    }
}

Filter Control in Controllers

<?php
class PublicController extends Controller 
{
    // Exclude specific filters
    public array $__ignoredFilters = [AuthFilter::class];
    
    // Or allow only specific filters
    public array $__allowedFilters = [CorsFilter::class];
}

DataObjects

Type-safe data serialization with automatic JSON conversion.

Configuration API

DataObjects use a fluent configuration API through the config() method:

<?php
use ExEngine\DataObject;
use ExEngine\DataObject\Config\DataObjectConfig;
use ExEngine\DataObject\Config\ExposeConfig;
use ExEngine\Enum\Data\SuppressNulls;

class Product extends DataObject 
{
    protected int $id;
    protected string $name;
    protected ?string $description = null;
    
    public function __construct(int $id, string $name) 
    {
        $this->id = $id;
        $this->name = $name;
        
        // Method 1: Full configuration
        $this->config(new DataObjectConfig(
            new ExposeConfig(
                SuppressNulls::TRUE,  // Suppress null values
                true                   // Add object name to output
            ),
            "ProductData"             // Custom object name
        ));
        
        // Method 2: Fluent configuration
        $this->config()->expose()->suppressNulls(SuppressNulls::FALSE);
        $this->config()->expose()->addObjectName(false);
        $this->config()->name("CustomName");
    }
}

SuppressNulls Options

  • SuppressNulls::TRUE - Always suppress null values
  • SuppressNulls::FALSE - Never suppress null values
  • SuppressNulls::GLOBAL - Use global config setting (default)

Basic Example

<?php
use ExEngine\DataObject;
use ExEngine\DataObject\Config\DataObjectConfig;
use ExEngine\DataObject\Config\ExposeConfig;
use ExEngine\Enum\Data\SuppressNulls;

class UserResponse extends DataObject 
{
    protected int $id;
    protected string $name;
    protected ?string $email;
    protected ?string $phone = null;
    
    public function __construct(int $id, string $name, ?string $email) 
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        
        // Configure DataObject behavior
        $this->config(new DataObjectConfig(
            new ExposeConfig(SuppressNulls::TRUE, false), // Suppress nulls, don't add object name
            "User" // Custom object name (optional)
        ));
    }
}

// In controller
public function getUser(): DataObject 
{
    return new UserResponse(1, "John Doe", "john@example.com");
}

Output:

{
    "took": 0,
    "code": 200,
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com",
        "_name": "User"
    },
    "error": false
}

Note: By default, DataObjects include a _name field with the object's class name or custom name. To disable this, set addObjectName to false in the ExposeConfig:

$this->config(new DataObjectConfig(
    new ExposeConfig(SuppressNulls::TRUE, false) // Second parameter disables _name field
));

Logging

ExEngine provides flexible logging with multiple implementations:

Enable Logging

<?php
// In your launcher file
const EXENGINE_LOG = true;           // Enable logging
const EXENGINE_LOG_WEB = true;       // Include errors in HTTP responses
const EXENGINE_TRACE = true;         // Enable detailed trace (development only)

Logger Types

ErrorLogLogger - Colored console output:

use ExEngine\Logger\ErrorLogLogger;
$logger = new ErrorLogLogger("MyApp");

JsonLogger - Structured JSON logs:

use ExEngine\Logger\JsonLogger;
$logger = new JsonLogger("MyApp");

MonoLogger - Monolog integration:

use ExEngine\Logger\MonoLogger;
$logger = new MonoLogger("MyApp");

Using Loggers

<?php
$logger = logger("MyController");

$logger->debug("Debug message", ["key" => "value"]);
$logger->info("Info message");
$logger->warning("Warning message");
$logger->error("Error message", [], $exception);
$logger->critical("Critical message");

// Create child loggers
$childLogger = $logger->child("SubModule");

Routing

URL Structure

http://domain.com/api.php/[Controller]/[Method]/[Arg1]/[Arg2]/...

Examples

URLControllerMethodArguments
/api.php/User/profile/123Userprofile["123"]
/api.php/Article/edit/456/draftArticleedit["456", "draft"]
/api.php/Admin/Dashboard/statsAdmin/Dashboardstats[]

Default Routing

Configure default route when no controller is specified:

<?php
use ExEngine\Config\Routing;
use ExEngine\Enum\Config\RoutingType;

// Redirect to controller method
$this->defaultRouting(
    new Routing(RoutingType::CONTROLLER, 'Home/index')
);

// Serve SPA (Single Page Application)
$this->defaultRouting(
    new Routing(RoutingType::SPA, '/path/to/index.html')
);

HTTP Method Validation

Ensure controller methods only accept specific HTTP methods:

<?php
use ExEngine\Controller;
use ExEngine\Enum\Http\HttpMethod;

class User extends Controller 
{
    public function create(): array 
    {
        // Only accept POST requests
        $this->accept(HttpMethod::POST);
        
        $name = $_POST['name'];
        return ["result" => "created", "name" => $name];
    }
    
    public function update(): array 
    {
        // Accept multiple methods
        $this->accept([HttpMethod::PUT, HttpMethod::PATCH]);
        
        return ["result" => "updated"];
    }
}

Redirects and Links

Redirects

<?php
// Redirect to method in same controller
$this->redirect('otherMethod', $arg1, $arg2);

// Redirect to another controller
$this->instance()->redirect('OtherController', 'method', $arg1);

// Permanent redirect (HTTP 301)
$this->redirectPermanent('newMethod');

// Redirect to URL
$this->instance()->redirect('https://example.com');
$this->instance()->redirect('/relative/path');

Generate Links

<?php
// Link to method in same controller
$url = $this->link('method', $arg1, $arg2);

// Link to another controller
$url = $this->instance()->link('Controller', 'method', $arg1);

// Link by URL
$url = $this->instance()->linkByUrl('path/to/resource');

Response Formats

Standard Response (Default)

{
    "took": 0,
    "code": 200,
    "data": {
        "key": "value"
    },
    "error": false
}

Raw Response

Return string to bypass standard response:

<?php
public function raw(): string 
{
    return "<h1>Raw HTML</h1>";
}

Or disable standard response in arrays:

<?php
public function custom(): array 
{
    return [
        "__useStandardResponse" => false,
        "custom": "format"
    ];
}

Output:

{
    "custom": "format"
}

Error Handling

Built-in Exceptions

  • ExEngine\NotFoundException - HTTP 404
  • ExEngine\RequestException - HTTP 400
  • ExEngine\ResponseException - HTTP 500
  • ExEngine\FilterException - Filter errors
  • ExEngine\ConfigException - Configuration errors
<?php
use ExEngine\NotFoundException;
use ExEngine\RequestException;

public function getUser(string $id): array 
{
    if (!is_numeric($id)) {
        throw new RequestException("Invalid user ID", ["id" => $id]);
    }
    
    $user = $this->db->find($id);
    if (!$user) {
        throw new NotFoundException("User not found");
    }
    
    return $user;
}

Custom Error Handler

<?php
class AppConfig extends Config 
{
    public function __construct(string $launcherPath) 
    {
        parent::__construct($launcherPath);
        
        // Redirect errors to custom handler
        $this->_errorHandler = 'Error/handle/{code}';
    }
}

Production Mode

Enable production mode for optimized performance:

<?php
class AppConfig extends Config 
{
    public function __construct(string $launcherPath) 
    {
        parent::__construct($launcherPath);
        
        // Enable production mode
        $this->_production = true;
        
        // Disable version info header
        $this->_showHeaderBanner = false;
    }
}

Production mode disables:

  • Runtime class/method validation checks
  • Development messages in responses
  • Detailed error traces in HTTP responses

Advanced Features

Component Late Initialization

Initialize singleton components after registration:

<?php
class AppConfig extends Config 
{
    public function componentLateInit(ExEngine $instance): void 
    {
        // Called after all components are registered
        // Use for cross-component initialization
        
        // You can manually trigger DI and configure components
        $injected = $instance->injector()->inject(MyComponent::class);
        $injected->configure();
        
        // Also, you can create the instance manually. DI will not be used in this case.
        $manuallyInstantiated = new MyComponent();
        
        // Register the configured instance as a singleton
        $this->component($injected, "configuredComponent");
        $this->component($manuallyInstantiated, "manuallyInstantiatedComponent");
    }
}

Development Messages

Add debug messages to responses (development mode only):

<?php
use ExEngine\ExEngine;

ExEngine::message("Debug info");
ExEngine::message(["key" => "value"]);

Response includes:

{
    "took": 0,
    "code": 200,
    "data": {...},
    "error": false,
    "developmentMessages": [
        "Debug info",
        {"key": "value"}
    ]
}

Global Instance Access

Access ExEngine instance from anywhere:

<?php
// Using helper function
$instance = ee();

// Using static method
$instance = ExEngine::instance();

// Access configuration
$config = ee()->config();

// Access filter data
$filterData = ee()->filtersData();

// Get controller metadata
$meta = ee()->meta();

// Access dependency injector
$injector = ee()->injector();

Examples

The framework includes comprehensive examples in the examples/ directory:

  • Controllers - Standard and REST controller examples
  • DependencyInjection - Component injection patterns
  • Filters - Request/Response filter implementations
  • Launcher - Various launcher configurations

Run examples locally:

cd examples/Controllers
./start.sh  # on Windows, please use WSL.

Database Support

ExEngine is database-agnostic. Use any ORM or database library:

  • Doctrine - Full-featured ORM
  • PDO - Direct database connections
  • Eloquent - Laravel's ORM (standalone)

Extensions & Ecosystem

Official Extensions

  • Zero Template - Lightweight template engine

Related Projects

Documentation

Generating API Documentation

ExEngine uses phpDocumentor to generate comprehensive HTML documentation from PHPDoc comments:

# Generate documentation
php phpDocumentor.phar --config=phpdoc.xml

# View documentation
open docs/api/index.html

The generated documentation includes:

  • Complete API reference for all classes and methods
  • Namespace and package organization
  • Inheritance diagrams
  • Cross-referenced code navigation
  • Search functionality

Support

Version Information

  • Current Version: 1.2.0
  • API Level: 3
  • Minimum PHP: 8.1.0

Migration from Previous Versions

ExEngine has evolved from a full MVC framework to a lightweight API-first microframework. If you're looking for the full MVC features from earlier versions, check out RadiancePHP, which provides a complete MVC framework built on top of ExEngine.

Contributing

Contributions are welcome! Please submit issues and pull requests to our GitLab repository.

License

The MIT License (MIT)

Copyright (c) 2018-2026 LinkFast S.A. (https://linkfast.io)
Copyright (c) 2016-2018 Giancarlo A. Chiappe Aguilar (gchiappe@linkfast.io)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.