Lynx is a high-performance, modern backend framework for PHP 8.1+ that brings the modular structure, decorator (attribute) system, and recursive dependency injection of TypeScript's popular NestJS framework to the PHP ecosystem.
The core differentiator of Lynx is its zero external Composer dependency design (Zero Composer Dependencies). Its engine and components are written entirely in pure PHP, enabling you to bootstrap applications without a bloated vendor/ directory while retaining full control over your codebase. (You can still integrate standard Composer libraries at any point if required).
- 1. Installation & CLI Setup (Compilation)
- 2. Project Directory Structure
- 3. Core Architecture & Autoloader
- 4. Decorators (Attributes) Reference
- 5. Dependency Injection (DI) & Encapsulation Boundaries
- 6. HTTP Request & Response API Reference
- 7. Database Engine & ORM (Repository Pattern)
- 8. Middleware & Conditional Execution
- 9. Error Handling & Exception Boundaries
- 10. Log Management
- 11. Helper Classes & Custom Encryption
- 12. Native Helper Functions Folder (
/php) - 13. Type Safety in Views
Lynx applications use a dedicated command-line interface written in Rust to bootstrap projects and generate structural templates (controllers, services, modules, etc.). Pre-compiled binaries are not distributed; developers must clone and build the tool locally.
- Clone the Repository:
git clone https://github.com/SignorMassimo/lynx_cli
- Build the Binary:
Make sure the Rust toolchain (Rustup/Cargo) is installed on your machine, then run the following in the repository root:
cd lynx-cli cargo build --release - Add to PATH:
Once compilation is complete, locate the compiled executable under
target/release/lynx(orlynx.exeon Windows) and add it to your system's environment variables (PATH).
- Create a New Project:
This command initializes the latest pre-configured Lynx application skeleton in the target directory.
lynx new project-name
- Generate a Controller:
lynx g controller <name> - Generate a Service / Provider:
lynx g service <name> - Generate a Module:
lynx g module <name> - Generate a Database Entity:
lynx g entity <name>
The standard file organization of a Lynx project is structured as follows:
├── App/ # Core framework engine (Do not modify)
│ ├── Builder/ # Structural builders (e.g., ExceptionBuilder)
│ ├── Cache/ # Runtime cache & system log directory
│ ├── Config/ # Log & system configurations
│ ├── Core/ # Engines (Router, Request, Response, LynxApplication, etc.)
│ ├── Decorators/ # Native PHP 8 attributes (Decorators)
│ ├── Exception/ # Core system & HTTP exceptions
│ ├── Helper/ # Console, Encryption, and System Utilities
│ ├── Interface/ # Interfaces (Middleware, ExceptionFilter, etc.)
│ └── Templates/ # Default error/fallback pages (e.g., notfound.php)
├── Src/ # Source directory for application code
│ ├── views/ # HTML/PHP view templates
│ ├── AppController.php # Core application controller
│ ├── AppMiddleware.php # Global application middleware
│ └── AppModule.php # Main application module definition
├── php/ # Directory for global procedural helper files
├── uploads/ # Default destination folder for uploaded files
├── .env # Configuration & database environment file
├── .htaccess # Apache web server routing configuration (URL Rewrite)
└── index.php # Application entry point (Bootstrap)
A Lynx application is bootstrapped in the root index.php file by loading App/Core/AutoLoader.php:
<?php
// index.php
require_once './App/Core/AutoLoader.php';
use App\Core\LynxApplication;
use Src\AppModule;
global $app;
$app = new LynxApplication(AppModule::class);
$app->run();- Global Error & Shutdown Handling:
- Configures PHP error reporting (
E_ALL) but disables raw error rendering (display_errors = 0). - Registers custom handlers (
set_error_handlerandregister_shutdown_function) to intercept notices, warnings, and fatal compilation/parse/runtime errors, mapping them into the framework'sApp\Core\Exceptionmodel. This yields structured JSON responses and terminal highlights for debugging.
- Configures PHP error reporting (
- Namespace Autoloading:
- Declares two discrete
spl_autoload_registerautoloader definitions to map namespaces without Composer:- Maps the
App\namespace prefix to the/Appfolder. - Maps the
Src\namespace prefix to the/Srcfolder.
- Maps the
- Declares two discrete
Lynx uses native PHP 8 attributes to attach routing and structural metadata to classes, methods, or constructor parameters:
Declares a modular context. Accepts a configuration array:
controllers: Controller classes registered in this module.providers: Services / providers to be registered in this module.imports(ormodules): Child modules imported into this module.exports: Registered providers exported for use in importing modules.entities: Database schemas (Entities) to register.middlewares: Module-scoped middlewares.
#[Module([
'imports' => [UserModule::class],
'controllers' => [AppController::class],
'providers' => [AppService::class],
'exports' => [AppService::class],
'entities' => [ProductEntity::class]
])]
class AppModule {}Designates a class as an HTTP routing controller. Accepts a route prefix:
#[Controller('/api/v1/products')]
class ProductController { ... }Binds controller methods to specific HTTP verbs:
#[Get(string $route = '')]#[Post(string $route = '')]#[Put(string $route = '')]#[Patch(string $route = '')]#[Delete(string $route = '')]#[Route(string $route, array $methods)](For binding to multiple HTTP verbs)
Example usage:
#[Get('/detail/:id')]
public function detail(Request $req, Response $res) { ... }Triggers dependency resolution for constructor parameters.
Applies middlewares to a class or a specific method:
#[Middleware([AuthMiddleware::class])]Maps classes and properties to database tables and columns:
#[Entity(string $tableName)]: Binds the class to a database table.#[Column(string $type, bool $nullable = false, ?int $length = null, bool $primary = false, bool $unique = false, ?string $default = null, ?array $enum = null)]: Configures column data attributes.
#[Entity('users')]
class User {
#[Column('INT', primary: true)]
public int $id;
#[Column('VARCHAR', length: 150, unique: true)]
public string $email;
#[Column('ENUM', enum: ['admin', 'user'], default: 'user')]
public string $role;
}Lynx features a fully recursive Dependency Injection (DI) container. By type-hinting dependency classes in constructor parameters, the container resolves, instantiates, and caches (singletons) dependencies dynamically via ReflectionClass.
Modularity is strictly enforced by the Router to protect structural integrity:
- A Controller or Service can only inject providers that are registered in its parent module's
providersarray, or exported in theexportsarray of another module imported into the current module. - If a dependency is injected without being registered or exported correctly, the framework throws a encapsulation violation
SystemExceptionat runtime:- Example message:
Error: The dependency 'X' needed by 'Y' is not accessible in the context of module 'Z'. You must register it as a provider or import a module that exports it.
- Example message:
Route methods receive instance arguments of App\Core\Request and App\Core\Response.
$req->body(property): Holds the parsed form or JSON payload as an object.application/jsonpayloads are automatically decoded.$req->params(property): Associative array containing route wildcards (e.g.,idfrom/users/:id).$req->statusCode(property): The request status code.body(string $key = null): Returns the body or a specific property of the body.isJson(): bool: Checks if the Content-Type isapplication/json.isPost(): bool/isGet(): bool: Verifies the HTTP verb.getQuery(string $key = null): Retrieves query parameter fields (?search=foo).getCookie(string $name): ?string: Returns the value of a cookie.hasCookie(string $name): bool: Verifies cookie presence.
render(string $view, array $data = []): self: Evaluates and includes the view template fromSrc/views/{$view}.php. Automatically injects these variables:$protocol:httporhttps$hostname: Server hostname (HTTP_HOST)$server_url: Root URL of the site$public: URL path mapping to/uploads/$public_cdn: Prefix for serving uploaded files via/cdn?file=$csrf: HTML string containing a hidden CSRF token input field (<input type="hidden" ...>)
send($data): self: Echoes raw output and terminates the process.json($data): self: SetsContent-Type: application/jsonand returns the data encoded in JSON.redirect(string $url, array $data = []): self: Redirects to a URL. The second argument can pass flash state (redirect_data) to the destination.back(): self: Redirects back to the referrer page (HTTP_REFERER).plainText(string $text): self: Renders flat text output.xml($data): self: Converts array maps into XML formats.jsonp(string $callback, $data): self: Wraps JSON outputs in a JS callback function.download(string $filePath, string $downloadName = null): void: Triggers file transfer downloads.uploadFile(string $inputName, string $destination): self|null: Moves uploaded files after validating.setCookie(...)/deleteCookie(...): Cookie management methods.setFlash(string $key, $message)/getFlash(string $key): Handles session-based one-time flash messages.setStatusCode(int $code): self: Set response HTTP code.setHeader(string $name, string $value): self: Adds custom headers.
Lynx features a built-in ORM that dynamically handles database migrations and mapping.
When DB_STATUS=active in .env, LynxQuery inspects all Entity classes tesciled inside modules.
- If a table is missing, it parses column metadata (
type,nullable,length,primary,unique,default,enum) and executesCREATE TABLE IF NOT EXISTScommands automatically.
Database actions are processed using a generic Repository class:
findAll(): array: Fetches all records, instantiates Entity objects, and returns them as an array.findBy(array $criteria): array: Filters queries based on criteria, returning mapped entities.findOne(array $criteria): ?object: Resolves the first matching record as an entity instance, ornull.create(array|object $data): object: Inserts a record and returns the hydrated entity.update(array|object $where, array|object $data): object: Modifies matching records and returns the updated state.delete(array $conditions): bool: Deletes records matching the conditions, returningtrueon success.
When mapping veritabanı records (mapToEntity), string date columns are automatically cast to native PHP DateTime objects if the entity class property is type-hinted as DateTime.
Custom middleware must implement the App\Interface\Middleware interface, which defines the use method contract:
namespace Src;
use App\Core\Request;
use App\Core\Response;
use App\Interface\Middleware;
class AppMiddleware implements Middleware
{
public function use(Request $req, Response $res, mixed $next): void
{
// Logic executing before request
$next($req, $res); // Proceed to next middleware or controller
}
}The #[Middleware] attribute accepts add and remove rules in its arguments parameters to scope routing filters:
add: Restricts middleware activation to specific routes.remove: Exempts specific routes from middleware evaluation.
// Active only on GET /admin
#[Middleware([AuthMiddleware::class], ['add' => [['GET', '/admin']]])]
class AdminController { ... }
// Evaluate LogMiddleware everywhere except POST /upload
#[Middleware([LogMiddleware::class], ['remove' => [['POST', '/upload']]])]
class ImageController { ... }Lynx provides a verbose error boundary system to streamline local development.
When an exception is thrown, App\Core\Exception intercepts the stack and determines the source line of code. It reads the source file and outputs a log block showing 5 lines of context before and after the error, underlining the exact line with ~ glyphs. This allows debugging immediately from console stdout.
App\Exception\ exposes HTTP error boundary classes representing status codes:
BadRequestException(400)UnauthorizedException(401)PaymentRequiredException(402)ForbiddenException(403)NotFoundException(404)MethodNotAllowedException(405)NotAcceptableException(406)RequestTimeoutException(408)ConflictException(409)GoneException(410)LengthRequiredException(411)PreconditionFailedException(412)PayloadTooLargeException(413)UnsupportedMediaTypeException(415)TooManyRequestsException(429)InternalServerErrorException(500)NotImplementedException(501)BadGatewayException(502)ServiceUnavailableException(503)GatewayTimeoutException(504)HttpException(Custom status code exception)SystemException(Framework execution runtime exception)
You can hook a class implementing ExceptionFilter to intercept uncaught application errors and format custom JSON structures globally:
use App\Interface\ExceptionFilter;
class CustomExceptionFilter implements ExceptionFilter
{
public function catch($e, Request $req, Response $res)
{
$res->setStatusCode($e->statusCode ?? 500)->json([
'status' => 'error',
'msg' => $e->message
]);
}
}
// Register inside index.php or during application bootstrap:
$app->useGlobalExceptionFilter(new CustomExceptionFilter());Logs are handled by the static App\Helper\Console class.
LogLevel::INFO(info)LogLevel::WARNING(warning)LogLevel::ERROR(error)LogLevel::CRITICAL(critical)LogLevel::TRACE(trace)LogLevel::SUCCESS(success)LogLevel::DEBUG(debug)LogLevel::DEFAULT(default)
Executing Console::log('level', ...messages):
- serializes parameters into standard JSON formats.
- Writes a unique JSON log file named
log_<microtime>_<random>.jsoninto the/App/Cache/logs/directory. This avoids lock collisions and facilitates log-parsing pipelines.
App\Helper\Helper provides a set of static utility methods for environmental variables, security, and objects:
getEnv(string $key, $default = null): Parses.envkey-value pairs.tokenGenerator(int $length, ?string $chars = null): string: Generates random token strings, ensuring the first character is never '0'.encrypt(string $data, ?array $options = null): ?string: Secures text using salted buffers, character shifts, and two-layer base64 operations. Runs self-tests during encryption to verify decryption accuracy.decrypt(string $data, ?array $options = null): ?string: Reverses operations and returns the plain string.isEqual(string $plain, string $hashedOrEncrypted, ?array $options = null): array: Assesses equivalence by comparing values plain, encrypted, or decrypted, returning['isEqual' => true/false, 'method' => 'matched_approach'].encryptObject(array $data, string $key)/decryptObject(array $data, string $key): Recursively encrypts or decrypts array indices and nested objects.
The /php directory in the project root is dedicated to housing global procedural PHP helper files (e.g., ip.php) that lie outside the object-oriented framework engine.
- Any procedural scripts stored in this folder run in application scope and can be referenced in controllers, templates, or services.
- Example (
/php/ip.php):Functions defined here are directly callable globally within your application classes.<?php function getClientIP() { $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } return trim($ip); }
To enable autocomplete, validation, and type safety for variables inside views (Src/views/*.php), we recommend defining PHPDoc annotations at the top of templates.
Because Response::render() extracts keys into local scopes, annotations help modern IDEs map variable contracts.
Example (Src/views/welcome.php):
<?php
/**
* @var string $public
* @var string $server_url
* @var string $name
* @var string $csrf
*/
?>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="<?= $public ?>css/style.css">
<title>Welcome - <?= $name ?></title>
</head>
<body>
<form action="<?= $server_url ?>logout" method="POST">
<?= $csrf ?>
<button type="submit">Sign Out</button>
</form>
</body>
</html>Lynx Framework is distributed as open-source software under the MIT License.