Added support for autoloading, event handlers, and more

This commit is contained in:
Gregory Magarshak
2026-07-20 12:16:01 -04:00
parent fe52e9a82b
commit 1672e0ce4e
5 changed files with 683 additions and 86 deletions
+242 -47
View File
@@ -31,7 +31,7 @@ But on **actual PHP workloads**, the bootstrap savings make this **25x faster
- [vs FrankenPHP and Swoole](#-vs-frankenphp-and-swoole)
- [Features](#-features)
- [Server Headers](#-server-headers--what-your-php-can-send)
- [Project Structure](#-project-structure)
- [For PHP Developers](#-for-php-developers--the-micro-framework)
- [Configuration](#-configuration)
- [Three Ways to Run](#-three-ways-to-run)
- [Building](#-building)
@@ -444,41 +444,85 @@ update themselves without any manual invalidation calls.
---
## 📂 Project Structure
## 📂 For PHP Developers — The Micro-Framework
A typical project looks like this:
Qbix Server isn't just a static file server with PHP bolted on. It's a micro-framework
where you **drop files into conventional directories** and things just work — classes
autoload, events fire handlers, views render templates. No configuration needed for
the basics.
### Project layout
```
myproject/
├── qbixserver.php ← server entry point (or use the PHAR)
├── qbixserver.php ← server entry point (or use the PHAR)
├── config/
│ └── server.json ← server configuration
├── web/ ← document root (publicly accessible)
│ ├── index.html
│ └── server.json ← server + app configuration
├── web/ ← document root (publicly accessible)
│ ├── index.html ← static files served directly
│ ├── style.css
│ ├── app.js
── api.php ← executed as PHP
│ └── uploads/ ← served as static files (or via X-Accel-Redirect)
└── classes/ ← your PHP classes (for preloading)
├── MyApp.php
├── MyApp/
── User.php
── Feed.php
└── Auth.php
└── vendor/ ← composer dependencies
└── autoload.php
│ ├── api.php ← PHP scripts executed on request
── uploads/
├── classes/ ← autoloaded classes (preloaded into workers)
│ ├── MyApp/
├── User.php ← MyApp\User or MyApp_User
│ │ ├── Feed.php
── Auth.php
── vendor/
└── autoload.php ← Composer autoloader (optional)
├── handlers/ ← event handlers (loaded on demand)
└── MyApp/
│ └── feed/
│ ├── post.php ← handles "MyApp/feed/post" event
│ └── validate.php ← handles "MyApp/feed/validate" event
└── views/ ← PHP templates for Q::view()
└── MyApp/
└── feed/
├── page.php
└── item.php
```
Only files under `web/` are accessible via HTTP. The `classes/` directory is for
code that runs inside your PHP scripts — and for classes you want preloaded into
the fork pool.
Only `web/` is accessible via HTTP. Everything else is server-side only.
### Preloading classes
### Classes — preloaded, both conventions
When you use `--workers=N`, the parent process loads classes before forking.
Workers inherit everything via copy-on-write — zero bootstrap cost per request.
Drop a PHP file in `classes/` and it's autoloaded. Both naming conventions work:
Configure preloading in `config/server.json`:
```php
<?php
// classes/MyApp/User.php — namespace style (PSR-4)
namespace MyApp;
class User {
public static function fromSession(): ?self { /* ... */ }
public static function find(string $id): ?self { /* ... */ }
}
```
```php
<?php
// classes/MyApp/Auth.php — underscore style (Qbix convention)
class MyApp_Auth {
static function check(): bool { return !empty($_SESSION['user_id']); }
}
```
Both are available immediately in your `web/*.php` scripts:
```php
<?php
// web/profile.php — both class styles work, no require needed
use MyApp\User;
$user = User::fromSession();
$isAdmin = MyApp_Auth::check();
```
The autoloader also bridges between conventions — if you define `MyApp_Auth`,
it's also accessible as `MyApp\Auth`, and vice versa.
**Preloading** loads classes into memory before forking workers, so there's zero
autoloader overhead per request:
```json
{
@@ -487,11 +531,9 @@ Configure preloading in `config/server.json`:
"preload": {
"autoload": "classes/vendor/autoload.php",
"classes": [
"MyApp",
"MyApp\\User",
"MyApp\\Feed",
"MyApp\\Auth",
"MyApp\\Database"
"MyApp_Auth"
]
}
}
@@ -500,37 +542,173 @@ Configure preloading in `config/server.json`:
```
```bash
# Start with 4 workers — classes loaded once, shared across all
php qbixserver.php --root=./web --port=8080 --workers=4
# Autoloader: autoload.php
# Preloaded: 3 classes
```
What happens at startup:
Classes are **eager** — loaded once at startup, shared across all workers via
copy-on-write. This is the "hot path" code that handles every request.
```
1. Parent includes classes/vendor/autoload.php
2. Parent loads each class in the preload list (triggers autoloader)
3. All class definitions, constants, and autoloader maps are now in memory
4. Parent calls pcntl_fork() × 4
5. Each worker inherits everything — ready to handle requests immediately
```
### Handlers — loaded on demand
Your `web/api.php` can now use `MyApp\User` or `MyApp\Feed` without any `require`
or autoloader overhead — the classes are already loaded:
Handlers are the opposite of classes: they're loaded **only when their event fires**.
Drop a file in `handlers/` and it's available as an event:
```php
<?php
// web/api.php — classes are already in memory from preloading
use MyApp\User;
use MyApp\Feed;
// handlers/MyApp/feed/post.php
// Handles the "MyApp/feed/post" event
// Function name = path with slashes replaced by underscores
$user = User::fromSession();
$items = Feed::latest(20);
function MyApp_feed_post(&$params, &$result) {
$title = $params['title'] ?? 'Untitled';
$userId = $params['userId'] ?? null;
// Validate, save to DB, whatever
$id = saveFeedPost($userId, $title);
$result = ['id' => $id, 'title' => $title, 'saved' => true];
return $result;
}
```
Fire it from anywhere:
```php
<?php
// web/api.php
$result = Q::event('MyApp/feed/post', [
'title' => $_POST['title'],
'userId' => $_SESSION['user_id'],
]);
header('Content-Type: application/json');
header('Cache-Control: public, max-age=60');
echo json_encode($items);
echo json_encode($result);
```
The handler file is `include`'d the first time the event fires, then the function
stays in memory. If the event never fires, the file is never loaded. This is ideal
for things like webhooks, admin actions, and error handlers — code that runs rarely
but needs to be available.
**Check if a handler exists:**
```php
if (Q::canHandle('MyApp/feed/post')) {
Q::event('MyApp/feed/post', $params);
}
```
### Before/after hooks
You can attach hooks to any event via config — useful for validation, logging,
access control, or cross-cutting concerns:
```json
{
"Q": {
"handlersBeforeEvent": {
"MyApp/feed/post": ["MyApp/feed/validate"]
},
"handlersAfterEvent": {
"MyApp/feed/post": ["MyApp/feed/notify"]
}
}
}
```
```php
<?php
// handlers/MyApp/feed/validate.php
function MyApp_feed_validate(&$params, &$result) {
if (empty($params['title'])) {
$result = ['error' => 'Title required'];
return false; // stops the event chain — main handler won't fire
}
}
```
```php
<?php
// handlers/MyApp/feed/notify.php
function MyApp_feed_notify(&$params, &$result) {
// Runs after the main handler
if (!empty($result['saved'])) {
sendNotification($params['userId'], "Post published: " . $result['title']);
}
}
```
The chain is: **before hooks → main handler → after hooks**. Any before hook
returning `false` stops the chain. This is the same pattern the full
[Qbix Platform](https://github.com/Qbix/Platform) uses — your handlers
work identically when you upgrade.
### Remote handlers
Handlers can also be URLs. If a handler name in the config starts with
`http://` or `https://`, the server POSTs the event parameters as JSON
to that URL instead of loading a local PHP file:
```json
{
"Q": {
"handlersAfterEvent": {
"MyApp/user/register": ["https://hooks.example.com/new-user"]
}
}
}
```
When `Q::event('MyApp/user/register', $params)` fires, the local handler
runs first, then the server POSTs `$params` as JSON to the remote URL.
This is webhooks built into the event system — no separate webhook
infrastructure needed.
### Views — PHP templates
Render PHP templates from the `views/` directory:
```php
<?php
// views/MyApp/feed/item.php
// Variables are extracted into scope from the $params array
?>
<article>
<h2><?= htmlspecialchars($title) ?></h2>
<p><?= htmlspecialchars($body) ?></p>
<time><?= $time ?></time>
</article>
```
```php
<?php
// web/feed.php
$items = MyApp\Feed::latest(10);
$html = '';
foreach ($items as $item) {
$html .= Q::view('MyApp/feed/item.php', $item);
}
echo Q::view('MyApp/feed/page.php', ['content' => $html]);
```
Views are just PHP files — full language access, no template DSL to learn.
### The philosophy
| | Loaded when | Lives in | Purpose |
|---|---|---|---|
| **Classes** | Startup (preloaded) | `classes/` | Models, services, utilities — your core code |
| **Handlers** | First event fire (on demand) | `handlers/` | Actions, hooks, webhooks — code that responds to events |
| **Views** | When rendered | `views/` | Templates — HTML with PHP |
| **Scripts** | When requested via HTTP | `web/` | Entry points — the "controller" layer |
| **Config** | Startup | `config/` | Settings, handler hooks, preload lists |
Classes are **eager**. Handlers are **lazy**. Scripts are **per-request**.
Views are **on-demand**. This gives you the right loading strategy for each
kind of code without thinking about it — just put files in the right directory.
### Workers: fork-per-request (truly shared-nothing)
Each worker handles exactly **one request**, then exits. The parent immediately
@@ -554,6 +732,17 @@ single-threaded — fine for development and lightweight APIs. Superglobals
variables persist (same as php-fpm). Use `--workers=N` in production for
full isolation.
### Growing into the full Qbix Platform
The conventions above — `classes/`, `handlers/`, `views/`, `config/` — are
the same ones the [Qbix Platform](https://github.com/Qbix/Platform) uses.
When your project outgrows the micro-framework and you need user accounts,
real-time streams, access control, payments, or a plugin system, you switch
to `--app` mode and everything you've written keeps working. Your classes
stay in `classes/`, your handlers stay in `handlers/`, your views stay in
`views/`. You just gain access to Streams, Users, Assets, and the rest of
the plugin ecosystem — without rewriting anything.
---
## ⚙️ Configuration
@@ -784,7 +973,7 @@ you need — the CDN handles the protocol upgrade.
## 📋 Requirements
**For qbixserver.php and PHAR:**
**Linux / macOS (recommended):**
- PHP 8.1 or later
- Extensions: `sockets`, `pcntl` (for signals + workers), `openssl` (for HTTPS)
@@ -801,6 +990,12 @@ sudo apt install php-cli php-sockets
- Nothing. The PHP runtime is included.
**Windows:** The server runs in single-threaded mode (`--workers=0` only).
Static files, PHP scripts, WebSocket, caching, compression, access control —
everything works. You lose fork-per-request isolation and signal-based graceful
shutdown, because `pcntl` doesn't exist on Windows. Good for development; for
production use Linux or macOS (or WSL).
---
## 📄 License
BIN
View File
Binary file not shown.
+5
View File
@@ -107,6 +107,11 @@ if (!$webDir || !is_dir($webDir)) {
exit(1);
}
// Initialize Q with the project root (parent of web/)
// This sets up autoloading from classes/ and handlers from handlers/
$projectRoot = dirname($webDir);
Q::init($projectRoot);
// ── Load config ─────────────────────────────────────
// Default server config
+396 -38
View File
@@ -1,11 +1,20 @@
<?php
/**
* Minimal Q shim for standalone Qbix Server.
* Standalone Q shim for Qbix Server.
*
* Provides just enough of the Q framework for Q_WebServer and its
* dependencies to function without the full Qbix Platform.
* When running inside the full Platform, this file is never loaded —
* the real Q class takes over.
* Provides the core Q framework functionality needed to run
* the server and user PHP scripts without the full Qbix Platform.
* When running inside the full Platform (--app mode), this file
* is never loaded — the real Q class takes over.
*
* Includes:
* - Autoloader for both underscore (Q_WebServer) and namespace (MyApp\User) styles
* - Q::ifset() for safe nested array/object access
* - Q::event() with handlers/ folder convention
* - Q::view() for rendering PHP templates
* - Q_Config for JSON config file loading
*
* @module Q
*/
if (!defined('DS')) define('DS', DIRECTORY_SEPARATOR);
@@ -13,26 +22,96 @@ if (!defined('DS')) define('DS', DIRECTORY_SEPARATOR);
class Q
{
/**
* Safe nested array access. Returns $default if any key is missing.
* Signature: Q::ifset($arr, 'key1', 'key2', ..., $default)
* Directories to search for classes/ and handlers/
* Set by the server at startup based on --root and project structure
* @property $paths
* @type array
* @static
*/
static function ifset(&$arr)
static $paths = array();
/**
* Safe nested array/object access. Returns $default if any key is missing.
*
* Q::ifset($arr, 'key1', 'key2', $default)
* Q::ifset($obj, 'prop', $default)
*
* @method ifset
* @static
* @param {&mixed} $ref The array or object to traverse
* @return {mixed}
*/
static function ifset(&$ref)
{
$args = func_get_args();
array_shift($args); // remove $arr
$default = array_pop($args); // last arg is default
$ref = &$arr;
foreach ($args as $key) {
if (!is_array($ref) || !array_key_exists($key, $ref)) {
return $default;
}
$ref = &$ref[$key];
$count = func_num_args();
if ($count <= 2) {
$args = func_get_args();
$def = isset($args[1]) ? $args[1] : null;
return isset($ref) ? $ref : $def;
}
return $ref;
$args = func_get_args();
$def = end($args);
$path = array_slice($args, 1, -1);
return self::getObject($ref, $path, $def);
}
/**
* JSON encode with error handling
* Get a value deep inside an array or object.
*
* Q::getObject($data, ['users', 'alice', 'email'], 'default')
*
* @method getObject
* @static
* @param {&mixed} $ref The array or object to traverse
* @param {array} $path Array of keys/properties to follow
* @param {mixed} $def Default if path not found
* @return {mixed}
*/
static function getObject(&$ref, $path, $def = null)
{
$cur = $ref;
foreach ($path as $key) {
if (is_array($cur)) {
if (!array_key_exists($key, $cur)) return $def;
$cur = $cur[$key];
} elseif (is_object($cur)) {
if (!isset($cur->$key)) return $def;
$cur = $cur->$key;
} else {
return $def;
}
}
return $cur;
}
/**
* Set a value deep inside a nested array, creating intermediate arrays as needed.
*
* Q::setObject(['users', 'alice', 'email'], 'alice@example.com', $data)
*
* @method setObject
* @static
* @param {array} $path
* @param {mixed} $value
* @param {&array} $dest The target array (modified by reference)
*/
static function setObject($path, $value, &$dest)
{
if (is_string($path)) $path = array($path);
$ref = &$dest;
foreach ($path as $key) {
if (!isset($ref[$key]) || !is_array($ref[$key])) {
$ref[$key] = array();
}
$ref = &$ref[$key];
}
$ref = $value;
}
/**
* JSON encode with unescaped slashes
* @method json_encode
* @static
*/
static function json_encode($value, $options = 0)
{
@@ -40,40 +119,302 @@ class Q
}
/**
* Fire an event. No-op in standalone mode.
* JSON decode wrapper
* @method json_decode
* @static
*/
static function event($name, $params = array(), $type = '')
static function json_decode($json, $assoc = false, $depth = 512, $options = 0)
{
// No event system in standalone mode
return null;
return json_decode($json, $assoc, $depth, $options);
}
// ── Event system ────────────────────────────────────
/**
* Fire an event. Looks for handler functions in handlers/ directory.
*
* Handler for "MyApp/feed/post" lives at:
* handlers/MyApp/feed/post.php
* And defines:
* function MyApp_feed_post($params) { ... }
*
* @method event
* @static
* @param {string} $eventName e.g. "MyApp/feed/post"
* @param {array} $params Parameters passed to the handler
* @param {string|boolean} $pure false=run handler, 'before'=before hooks only,
* 'after'=after hooks only, true=both hooks but skip main handler
* @param {boolean} $skipIncludes If true, only call already-defined functions
* @param {mixed} &$result Reference for handlers to modify
* @return {mixed} Whatever the handler returned
*/
static function event(
$eventName,
$params = array(),
$pure = false,
$skipIncludes = false,
&$result = null)
{
if (!is_string($eventName) || !$eventName) return null;
if (!is_array($params)) $params = array();
// Before hooks
if ($pure !== 'after') {
$handlers = Q_Config::get('Q', 'handlersBeforeEvent', $eventName, array());
if (is_string($handlers)) $handlers = array($handlers);
if (is_array($handlers)) {
foreach ($handlers as $handler) {
$r = self::handle($handler, $params, $skipIncludes, $result);
if ($r === false) return $result;
}
}
}
// Main handler
if (!$pure) {
$result = self::handle($eventName, $params, $skipIncludes, $result);
}
// After hooks
if ($pure !== 'before') {
$handlers = Q_Config::get('Q', 'handlersAfterEvent', $eventName, array());
if (is_string($handlers)) $handlers = array($handlers);
if (is_array($handlers)) {
foreach ($handlers as $handler) {
$r = self::handle($handler, $params, $skipIncludes, $result);
if ($r === false) return $result;
}
}
}
return $result;
}
/**
* Autoloader for Q_* classes
* Check if a handler exists for an event name
* @method canHandle
* @static
* @param {string} $eventName
* @return {boolean}
*/
static function canHandle($eventName)
{
$parts = explode('/', $eventName);
$funcName = str_replace('-', '_', implode('_', $parts));
if (function_exists($funcName)) return true;
// Try to load from handlers/ directory
$relPath = 'handlers' . DS . implode(DS, $parts) . '.php';
foreach (self::$paths as $base) {
$full = $base . DS . $relPath;
if (file_exists($full)) {
include_once $full;
return function_exists($funcName);
}
}
return false;
}
/**
* Execute a handler function. Loads from handlers/ directory if needed.
* If $eventName starts with http:// or https://, POSTs params as JSON
* to that URL (remote handler / webhook).
* @method handle
* @static
* @param {string} $eventName
* @param {array} &$params
* @param {boolean} $skipIncludes
* @param {mixed} &$result
* @return {mixed}
*/
protected static function handle(
$eventName, &$params = array(), $skipIncludes = false, &$result = null)
{
if (!$eventName) return null;
// Remote handler — POST params as JSON to URL
if (strncmp($eventName, 'http://', 7) === 0
|| strncmp($eventName, 'https://', 8) === 0
) {
return self::handleRemote($eventName, $params, $result);
}
$parts = explode('/', $eventName);
$funcName = str_replace('-', '_', implode('_', $parts));
if (!function_exists($funcName)) {
if ($skipIncludes) return null;
// Try to load from handlers/ directory
$relPath = 'handlers' . DS . implode(DS, $parts) . '.php';
$loaded = false;
foreach (self::$paths as $base) {
$full = $base . DS . $relPath;
if (file_exists($full)) {
include_once $full;
$loaded = true;
break;
}
}
if (!$loaded || !function_exists($funcName)) {
return null; // no handler found — that's OK
}
}
$args = array(&$params, &$result);
return call_user_func_array($funcName, $args);
}
/**
* POST event params as JSON to a remote URL.
* Used for webhook-style handlers configured in Q.handlersAfterEvent.
* Non-blocking: uses a short timeout so it doesn't slow down the request.
* @method handleRemote
* @static
* @param {string} $url
* @param {array} &$params
* @param {mixed} &$result
* @return {mixed}
*/
protected static function handleRemote($url, &$params, &$result)
{
$json = json_encode($params, JSON_UNESCAPED_SLASHES);
$opts = array('http' => array(
'method' => 'POST',
'header' => "Content-Type: application/json\r\n"
. "Content-Length: " . strlen($json) . "\r\n"
. "User-Agent: QbixServer/1.0\r\n",
'content' => $json,
'timeout' => 5,
'ignore_errors' => true,
));
$ctx = stream_context_create($opts);
$response = @file_get_contents($url, false, $ctx);
if ($response !== false) {
$decoded = json_decode($response, true);
if ($decoded !== null) {
$result = $decoded;
}
}
return $result;
}
/**
* Render a PHP view file. Searches views/ directories in $paths.
*
* echo Q::view('MyApp/feed/page.php', ['items' => $items]);
*
* @method view
* @static
* @param {string} $viewName Path relative to views/ directory
* @param {array} $params Variables extracted into the view scope
* @return {string} Rendered HTML
*/
static function view($viewName, $params = array())
{
$viewPath = str_replace('/', DS, $viewName);
foreach (self::$paths as $base) {
$full = $base . DS . 'views' . DS . $viewPath;
if (file_exists($full)) {
extract($params);
ob_start();
include $full;
return ob_get_clean();
}
}
return "<!-- view not found: $viewName -->";
}
// ── Autoloader ──────────────────────────────────────
/**
* Autoloader that handles both conventions:
* Q_WebServer → classes/Q/WebServer.php (underscore)
* MyApp\User → classes/MyApp/User.php (namespace)
* MyApp_Helper → classes/MyApp/Helper.php (underscore)
*
* Searches the src/ directory (for Q_ server classes) and all
* directories in Q::$paths (for user classes).
*
* @method autoload
* @static
* @param {string} $className
*/
static function autoload($className)
{
if (strpos($className, 'Q_') !== 0 && $className !== 'Q_Config') return;
$path = str_replace('_', DS, $className) . '.php';
$full = dirname(__FILE__) . DS . $path;
if (file_exists($full)) {
require_once $full;
// Split on both \ and _ to get path parts
$parts = array();
foreach (explode('\\', $className) as $nsPart) {
$parts = array_merge($parts, explode('_', $nsPart));
}
$relPath = implode(DS, $parts) . '.php';
// 1. Search src/ directory (for Q_* server classes)
$srcPath = dirname(__FILE__) . DS . $relPath;
if (file_exists($srcPath)) {
require_once $srcPath;
return;
}
// 2. Search project classes/ directories
foreach (self::$paths as $base) {
$full = $base . DS . 'classes' . DS . $relPath;
if (file_exists($full)) {
require_once $full;
// If loaded via underscore but also accessible via namespace, alias
$underscoreName = implode('_', $parts);
$namespaceName = implode('\\', $parts);
if ($underscoreName !== $namespaceName) {
if (class_exists($underscoreName, false)
&& !class_exists($namespaceName, false)
) {
class_alias($underscoreName, $namespaceName);
} elseif (class_exists($namespaceName, false)
&& !class_exists($underscoreName, false)
) {
class_alias($namespaceName, $underscoreName);
}
}
return;
}
}
}
/**
* Initialize Q paths from the project root directory.
* Called by the server at startup.
* @method init
* @static
* @param {string} $projectRoot The project root (parent of web/)
*/
static function init($projectRoot)
{
$projectRoot = rtrim($projectRoot, DS);
if (!in_array($projectRoot, self::$paths)) {
self::$paths[] = $projectRoot;
}
}
}
spl_autoload_register(array('Q', 'autoload'));
// ── Q_Config ────────────────────────────────────────
/**
* Minimal Q_Config — reads JSON config files merged together.
* JSON config file loader with deep merge.
* Compatible with the full Qbix Platform's Q_Config API.
*
* @class Q_Config
*/
class Q_Config
{
private static $data = array();
private static $loaded = false;
/**
* Load config from JSON file(s)
* Load and merge a JSON config file
* @method load
* @static
* @param {string} $path Path to JSON file
*/
static function load($path)
{
@@ -82,11 +423,13 @@ class Q_Config
if (is_array($json)) {
self::$data = self::merge(self::$data, $json);
}
self::$loaded = true;
}
/**
* Set a config value programmatically
* Set a config value programmatically.
* Q_Config::set('Q', 'webserver', 'port', 8080)
* @method set
* @static
*/
static function set(/* key1, key2, ..., value */)
{
@@ -103,8 +446,12 @@ class Q_Config
}
/**
* Get a config value with default.
* Q_Config::get('Q', 'webserver', 'keepAlive', 'max', 100)
* Get a config value with a default.
* Q_Config::get('Q', 'webserver', 'keepAlive', 'max', 100)
* Last argument is the default.
* @method get
* @static
* @return {mixed}
*/
static function get(/* key1, key2, ..., default */)
{
@@ -121,7 +468,12 @@ class Q_Config
}
/**
* Get a config value or throw.
* Get a config value or throw if missing.
* Q_Config::expect('Q', 'app')
* @method expect
* @static
* @return {mixed}
* @throws {Exception}
*/
static function expect(/* key1, key2, ... */)
{
@@ -138,6 +490,9 @@ class Q_Config
/**
* Get all config data
* @method getAll
* @static
* @return {array}
*/
static function getAll()
{
@@ -145,7 +500,10 @@ class Q_Config
}
/**
* Deep merge arrays (scalars overwrite, arrays merge recursively)
* Deep merge: arrays merge recursively, scalars overwrite.
* @method merge
* @static
* @private
*/
private static function merge($base, $overlay)
{
+39
View File
@@ -92,6 +92,45 @@ class Q_WebServer
}
}
// ── Preload classes (before forking) ─────────────
$preload = Q_Config::get('Q', 'webserver', 'preload', array());
if (!empty($preload)) {
// Load the autoloader first (e.g. Composer's)
$autoload = is_string($preload)
? $preload
: (isset($preload['autoload']) ? $preload['autoload'] : null);
if ($autoload) {
$autoloadPath = $autoload;
// Resolve relative to the document root's parent (project root)
if ($autoloadPath[0] !== '/' && $autoloadPath[0] !== '\\') {
$projectRoot = dirname(rtrim(self::$rootDir, DS));
$autoloadPath = $projectRoot . DS . $autoloadPath;
}
if (file_exists($autoloadPath)) {
require_once $autoloadPath;
$count = count(get_declared_classes());
echo " Autoloader: " . basename($autoload) . "\n";
} else {
echo " Warning: autoload file not found: $autoloadPath\n";
}
}
// Then load each named class (triggers the autoloader)
$classes = isset($preload['classes']) ? $preload['classes'] : array();
if (!empty($classes)) {
$loaded = 0;
foreach ($classes as $class) {
if (!class_exists($class, true) && !interface_exists($class, true)
&& !trait_exists($class, true)
) {
echo " Warning: could not preload $class\n";
} else {
$loaded++;
}
}
echo " Preloaded: $loaded classes\n";
}
}
// ── Worker pool ──────────────────────────────────
if ($workers > 0 && function_exists('pcntl_fork')) {
self::$pool = new Q_WebServer_Pool($workers);