APP DATA:\n";
print_r(self::$app);
echo "";
} else if ($key === 1) {
echo "
FUNCTIONS:\n";
print_r(array_keys(self::$fns));
echo "";
} else {
$val = self::find($key);
echo "";
if (is_array($val)) {
print_r($val);
} else {
echo $val;
}
echo "";
}
}
// Internal helper
private static function &find($key, $remove = false, $fns_mode = false)
{
if ($fns_mode) {
$ref = &self::$fns;
} else {
$ref = &self::$app;
}
if (strpos($key, ".") > -1) {
$ps = explode('.', $key);
$c = count($ps);
foreach ($ps as $part) {
$c--;
if ($remove && $c === 0) {
unset($ref[$part]);
$null = null;
return $null;
} else {
if (!isset($ref[$part])) {
$ref[$part] = array();
}
$ref = &$ref[$part];
}
}
return $ref;
} else {
if ($remove) {
unset($ref[$key]);
$null = null;
return $null;
} else {
if (!isset($ref[$key])) {
$ref[$key] = null;
}
return $ref[$key];
}
}
}
}
// ============================================================================
// CONSTANTS
// ============================================================================
defined('DS') or define('DS', DIRECTORY_SEPARATOR);
defined('GENES_DIR') or define('GENES_DIR', __DIR__);
defined('PROJECT_ROOT') or define('PROJECT_ROOT', dirname(GENES_DIR) . DS);
defined('WORK_DIR') or define('WORK_DIR', getcwd() . DS);
// Folders
defined('DATA_FOLDER') or define('DATA_FOLDER', WORK_DIR . 'data' . DS);
defined('CACHE_FOLDER') or define('CACHE_FOLDER', WORK_DIR . 'cache' . DS);
defined('UI_FOLDER') or define('UI_FOLDER', WORK_DIR . 'ui' . DS);
defined('MODS_FOLDER') or define('MODS_FOLDER', WORK_DIR . 'mods' . DS);
defined('UPLOADS_FOLDER') or define('UPLOADS_FOLDER', WORK_DIR . 'uploads' . DS);
// Files
defined('CONFIG_FILE') or define('CONFIG_FILE', DATA_FOLDER . 'config.json');
defined('LOG_FILE') or define('LOG_FILE', DATA_FOLDER . 'system.log');
defined('UI_TEMPLATE') or define('UI_TEMPLATE', UI_FOLDER . 'index.html');
// URLs
defined('GENES_CDN_URL') or define('GENES_CDN_URL', 'https://cdn.genes.one/');
// Set default timezone early to prevent warnings
@date_default_timezone_set('UTC');
// ============================================================================
// CORE FUNCTIONS
// ============================================================================
g::def("core", array(
"init" => function () {
g::set("system.time.start", microtime(true));
g::set("system.memory.start", memory_get_usage());
g::run("core.prepareConfig");
g::run("core.detectDomain");
g::run("core.setEnvironment");
g::run("core.createFolders");
g::run("core.setApplicationInfo");
g::run("core.autoConnectDatabase");
g::run("core.autoInitializeData");
g::set("system.initialized", true);
$time = g::run("core.timestamp");
g::run("core.log", "[INIT] Framework initialized at $time");
},
"prepareConfig" => function () {
// Detect current environment URL
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
// Detect base path from SCRIPT_NAME
$basePath = '';
if (isset($_SERVER['SCRIPT_NAME'])) {
$scriptPath = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
if ($scriptPath !== '/' && $scriptPath !== '.') {
$basePath = $scriptPath;
}
}
// Build full base URL
$baseUrl = $protocol . '://' . $host . $basePath;
if (substr($baseUrl, -1) !== '/') {
$baseUrl .= '/';
}
// Create URL-safe environment key (remove protocol and special chars)
$envKey = str_replace(array('://', '/', '\\', ':'), '_', $baseUrl);
$envKey = trim($envKey, '_');
// Just use domain for clone.domain (without path)
$cloneDomain = $host;
$defaults = array(
"clone" => array(
"name" => "My Clone",
"domain" => $cloneDomain,
"type" => "platform",
"state" => "active",
"settings" => array(
"theme" => "default",
"features" => array()
)
),
"urls" => array(
"cdn" => GENES_CDN_URL
),
"settings" => array(
"debug" => false,
"timezone" => "UTC",
"language" => "en",
"date_format" => "Y-m-d H:i:s.u",
"log_level" => 1,
"session_name" => "genes_session",
"environment" => "development",
),
"application" => array(
"name" => "Genes App",
"version" => "1.0.0",
"description" => "Built with Genes Framework",
),
"meta" => array(
"title" => "Genes App",
"description" => "Built with Genes Framework",
"keywords" => "",
"og_image" => "ui/img/share.jpg"
),
"database" => array(
"enabled" => false,
"type" => "mysql",
"host" => "127.0.0.1",
"port" => 3306,
"name" => "",
"user" => "",
"password" => "",
"charset" => "utf8mb4",
),
"security" => array(
"salt" => "",
"secret" => "",
),
"admin" => array(
"name" => "Admin User",
"alias" => "admin",
"email" => "admin@localhost",
"password" => "admin123",
"type" => "admin",
"state" => "active"
),
"seed" => array(
"labels" => array(
// Add your custom labels here
// Example: array("name" => "label.custom", "label" => "Custom", "value" => "custom")
)
),
"setup" => array(
"completed" => false
),
"views" => array(
"Index" => array(
"function" => "clone.Index",
"template" => "index",
"urls" => array(
"en" => "index"
)
),
"Admin" => array(
"function" => "clone.Admin",
"template" => "admin",
"urls" => array(
"en" => "admin"
)
)
),
"bits" => array(
"site_name" => array(
"en" => "My Site"
)
),
"env" => array(
$envKey => array(
"base_url" => $baseUrl,
"paths" => array(
"data" => DATA_FOLDER,
"cache" => CACHE_FOLDER,
"ui" => UI_FOLDER,
"mods" => MODS_FOLDER,
"uploads" => UPLOADS_FOLDER,
)
)
)
);
if (file_exists(CONFIG_FILE)) {
$json = file_get_contents(CONFIG_FILE);
$userConfig = json_decode($json, true);
if ($userConfig && is_array($userConfig)) {
$config = g::run("core.arrayMergeRecursive", $defaults, $userConfig);
} else {
$config = $defaults;
g::run("core.log", "[WARNING] Could not parse config.json");
}
} else {
$config = $defaults;
if (empty($config["security"]["salt"])) {
$config["security"]["salt"] = g::run("core.generateRandomKey", 64);
}
if (empty($config["security"]["secret"])) {
$config["security"]["secret"] = g::run("core.generateRandomKey", 64);
}
g::run("core.saveConfig", $config);
}
// Detect and merge environment-specific config
if (isset($config["env"]) && is_array($config["env"])) {
// Detect current environment key (same logic as default generation)
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
$basePath = '';
if (isset($_SERVER['SCRIPT_NAME'])) {
$scriptPath = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
if ($scriptPath !== '/' && $scriptPath !== '.') {
$basePath = $scriptPath;
}
}
$baseUrl = $protocol . '://' . $host . $basePath;
if (substr($baseUrl, -1) !== '/') {
$baseUrl .= '/';
}
$currentEnvKey = str_replace(array('://', '/', '\\', ':'), '_', $baseUrl);
$currentEnvKey = trim($currentEnvKey, '_');
// Try to find matching environment
$envConfig = null;
if (isset($config["env"][$currentEnvKey])) {
$envConfig = $config["env"][$currentEnvKey];
g::run("core.log", "[ENV] Matched environment: $currentEnvKey");
}
if ($envConfig) {
// Store base_url in config
if (isset($envConfig["base_url"])) {
$config["base_url"] = $envConfig["base_url"];
}
// Merge environment paths into main config
if (isset($envConfig["paths"])) {
if (!isset($config["paths"])) {
$config["paths"] = array();
}
$config["paths"] = array_merge($config["paths"], $envConfig["paths"]);
}
// Merge environment URLs into main config
if (isset($envConfig["urls"])) {
$config["urls"] = array_merge($config["urls"], $envConfig["urls"]);
}
g::run("core.log", "[ENV] Loaded environment config: $currentEnvKey");
} else {
g::run("core.log", "[ENV] No specific environment config found, using defaults");
}
}
g::set("config", $config);
},
"setEnvironment" => function () {
$config = g::get("config");
$timezone = isset($config["settings"]["timezone"]) ? $config["settings"]["timezone"] : "UTC";
date_default_timezone_set($timezone);
$env = isset($config["settings"]["environment"]) ? $config["settings"]["environment"] : "production";
if ($env === "development") {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
ini_set('display_errors', '0');
}
ini_set('default_charset', 'UTF-8');
g::run("core.log", "[ENV] Environment set: $env, timezone: $timezone");
},
"createFolders" => function () {
$folders = array(
DATA_FOLDER,
CACHE_FOLDER,
UI_FOLDER,
MODS_FOLDER,
UPLOADS_FOLDER,
);
foreach ($folders as $folder) {
if (!is_dir($folder)) {
if (mkdir($folder, 0755, true)) {
g::run("core.log", "[FOLDER] Created: $folder");
} else {
g::run("core.log", "[ERROR] Could not create folder: $folder");
}
}
}
$htaccess = DATA_FOLDER . '.htaccess';
if (!file_exists($htaccess)) {
$content = "# Deny all access\nDeny from all";
file_put_contents($htaccess, $content);
}
$protectedFolders = array(DATA_FOLDER, MODS_FOLDER);
foreach ($protectedFolders as $folder) {
$indexFile = $folder . 'index.html';
if (!file_exists($indexFile)) {
file_put_contents($indexFile, '');
}
}
// Create root .htaccess for routing
g::run("core.createRootHtaccess");
},
"createRootHtaccess" => function () {
// Determine if index.php is in current directory or we're in a subfolder
$scriptDir = dirname($_SERVER['SCRIPT_FILENAME']);
$htaccessFile = $scriptDir . DIRECTORY_SEPARATOR . '.htaccess';
// Only create if it doesn't exist
if (!file_exists($htaccessFile)) {
$content = "# Genes Framework - URL Rewriting\n# Portable configuration - works in any directory without modification\n\n\n RewriteEngine On\n \n # Redirect all requests to index.php except existing files/directories\n RewriteCond %{REQUEST_FILENAME} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteRule ^(.*)$ index.php [QSA,L]\n\n\n# Security Headers\n\n Header set X-Content-Type-Options \"nosniff\"\n Header set X-Frame-Options \"SAMEORIGIN\"\n Header set X-XSS-Protection \"1; mode=block\"\n\n\n# Disable directory browsing\nOptions -Indexes\n\n# Protect sensitive files\n\n Order allow,deny\n Deny from all\n";
$result = file_put_contents($htaccessFile, $content);
if ($result !== false) {
g::run("core.log", "[HTACCESS] Created portable .htaccess at: $htaccessFile");
} else {
g::run("core.log", "[ERROR] Could not create .htaccess file");
}
}
},
"setApplicationInfo" => function () {
$config = g::get("config");
$appName = isset($config["application"]["name"]) ? $config["application"]["name"] : "Genes App";
$appVersion = isset($config["application"]["version"]) ? $config["application"]["version"] : "1.0.0";
$appDesc = isset($config["application"]["description"]) ? $config["application"]["description"] : "";
$appEnv = isset($config["settings"]["environment"]) ? $config["settings"]["environment"] : "production";
g::set("app.name", $appName);
g::set("app.version", $appVersion);
g::set("app.description", $appDesc);
g::set("app.environment", $appEnv);
},
"timestamp" => function ($timestamp = null) {
if ($timestamp === null) {
$timestamp = microtime(true);
}
$seconds = floor($timestamp);
$milliseconds = sprintf("%03d", round(($timestamp - $seconds) * 1000));
$format = g::get("config.settings.date_format");
if (!$format) {
$format = "Y-m-d H:i:s";
}
// Replace .u with milliseconds, or append if no .u found
if (strpos($format, '.u') !== false) {
$formatted = date($format, $seconds);
$formatted = str_replace('.u', '.' . $milliseconds, $formatted);
} else {
$formatted = date($format, $seconds) . '.' . $milliseconds;
}
return $formatted;
},
/**
* Detect current domain from config rules (no hardcoded domains!)
*/
"detectDomain" => function () {
$config = g::get("config");
// Get domain detection rules from config
$domainRules = isset($config["domain_rules"]) ? $config["domain_rules"] : array();
$defaultDomain = isset($config["default_domain"]) ? $config["default_domain"] : null;
// Get current host
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
$host = preg_replace('/:\d+$/', '', $host); // Remove port
// Get subdirectory from REQUEST_URI (not SCRIPT_NAME)
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
$requestPath = parse_url($requestUri, PHP_URL_PATH);
$pathParts = array_filter(explode('/', trim($requestPath, '/')));
$subdir = !empty($pathParts) ? $pathParts[0] : '';
// Build full identifier (host + subdir for localhost)
$fullIdentifier = $host;
if ($subdir && strpos($host, 'localhost') !== false) {
$fullIdentifier = $host . '/' . $subdir;
}
// Match against rules
foreach ($domainRules as $pattern => $domainKey) {
// Simple exact match
if ($pattern === $fullIdentifier) {
g::set("current_domain", $domainKey);
g::run("core.log", "[DOMAIN] Detected: $domainKey (matched: $pattern)");
return $domainKey;
}
// Wildcard match (e.g., *.expo.live)
if (strpos($pattern, '*') !== false && fnmatch($pattern, $fullIdentifier)) {
g::set("current_domain", $domainKey);
g::run("core.log", "[DOMAIN] Detected: $domainKey (wildcard: $pattern)");
return $domainKey;
}
}
// Fallback to default
if ($defaultDomain) {
g::set("current_domain", $defaultDomain);
g::run("core.log", "[DOMAIN] Using default: $defaultDomain");
return $defaultDomain;
}
// Ultimate fallback
g::set("current_domain", "expo.live");
g::run("core.log", "[DOMAIN] Ultimate fallback: expo.live");
return "expo.live";
},
"arrayMergeRecursive" => function ($array1, $array2) {
foreach ($array2 as $key => $value) {
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
$array1[$key] = g::run("core.arrayMergeRecursive", $array1[$key], $value);
} else {
$array1[$key] = $value;
}
}
return $array1;
},
"generateRandomKey" => function ($length = 32, $type = "alnum") {
$charsets = array(
"hex" => "0123456789abcdef",
"alpha" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"alnum" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
"full" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:,.<>?",
);
$charset = isset($charsets[$type]) ? $charsets[$type] : $charsets["alnum"];
$charsetLength = strlen($charset);
$key = "";
for ($i = 0; $i < $length; $i++) {
$key .= $charset[mt_rand(0, $charsetLength - 1)];
}
return $key;
},
"getBaseUrl" => function () {
// Detect protocol - check multiple ways for HTTPS
$isHttps = false;
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
$isHttps = true;
} else if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$isHttps = true;
} else if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') {
$isHttps = true;
} else if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
$isHttps = true;
}
$protocol = $isHttps ? 'https://' : 'http://';
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
// Get the detected base path from request (includes domain identifier)
$request = g::get("request");
$basePath = '';
// First check if we have a domain_base_path (the subdirectory used for domain detection)
if ($request && isset($request['domain_base_path']) && $request['domain_base_path']) {
$basePath = $request['domain_base_path'];
} else if ($request && isset($request['path'])) {
// Fallback: Extract base path by comparing REQUEST_URI with the cleaned path
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
$pathOnly = parse_url($requestUri, PHP_URL_PATH);
$cleanPath = $request['path'];
// The base path is what was stripped: REQUEST_URI - clean path
if ($pathOnly !== $cleanPath && strpos($pathOnly, $cleanPath) !== false) {
$basePath = substr($pathOnly, 0, strlen($pathOnly) - strlen($cleanPath));
$basePath = rtrim($basePath, '/');
}
}
// Ensure trailing slash
if ($basePath && substr($basePath, -1) !== '/') {
$basePath .= '/';
} elseif (!$basePath) {
$basePath = '/';
}
return $protocol . $host . $basePath;
},
"saveConfig" => function ($config) {
if (!is_dir(DATA_FOLDER)) {
mkdir(DATA_FOLDER, 0755, true);
}
$json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$result = file_put_contents(CONFIG_FILE, $json);
if ($result !== false) {
g::run("core.log", "[CONFIG] Configuration saved to " . CONFIG_FILE);
return true;
} else {
g::run("core.log", "[ERROR] Could not save configuration to " . CONFIG_FILE);
return false;
}
},
"log" => function ($message, $level = 3) {
$configLevel = g::get("config.settings.log_level");
if (!$configLevel) {
$configLevel = 1;
}
if ($level > $configLevel) {
return;
}
if (!is_dir(DATA_FOLDER)) {
mkdir(DATA_FOLDER, 0755, true);
}
$timestamp = g::run("core.timestamp");
$logEntry = "[$timestamp] $message\n";
file_put_contents(LOG_FILE, $logEntry, FILE_APPEND);
},
/**
* Auto-connect to database if enabled in config
*/
"autoConnectDatabase" => function () {
$config = g::get("config");
if (!isset($config["database"]) || !isset($config["database"]["enabled"])) {
return false;
}
if ($config["database"]["enabled"] !== true) {
return false;
}
$dbConfig = $config["database"];
$params = array(
"driver" => isset($dbConfig["type"]) ? $dbConfig["type"] : "mysql",
"name" => "default"
);
if ($params["driver"] === "sqlite") {
$params["database"] = isset($dbConfig["database"]) ? $dbConfig["database"] : "data/database.db";
} else {
$params["host"] = isset($dbConfig["host"]) ? $dbConfig["host"] : "localhost";
$params["port"] = isset($dbConfig["port"]) ? $dbConfig["port"] : 3306;
$params["database"] = isset($dbConfig["name"]) ? $dbConfig["name"] : "";
$params["username"] = isset($dbConfig["user"]) ? $dbConfig["user"] : "root";
$params["password"] = isset($dbConfig["password"]) ? $dbConfig["password"] : "";
if (isset($dbConfig["charset"])) {
$params["charset"] = $dbConfig["charset"];
}
}
$connected = g::run("db.connect", $params);
if ($connected) {
g::run("db.createSchema");
g::run("core.log", "[DB] Auto-connected and schema created");
return true;
} else {
g::run("core.log", "[DB] Auto-connect failed");
return false;
}
},
/**
* Auto-initialize clone and admin data from config
* DEPRECATED: Now handled by db.autoSeed (called automatically after schema creation)
*/
"autoInitializeData" => function () {
// No longer needed - db.autoSeed handles this automatically
return true;
},
));
// ============================================================================
// PHASE 2: CONFIGURATION MANAGEMENT
// ============================================================================
g::def("config", array(
/**
* Get the current configuration
*
* Returns the entire loaded configuration object.
*
* @return array Configuration array
*
* @example $config = g::run("config.get");
* @example echo $config['settings']['timezone'];
*/
"get" => function () {
return g::get("config");
},
/**
* Read and decode a JSON file
*
* Safely reads a JSON file and returns decoded array.
* Returns false if file doesn't exist or JSON is invalid.
*
* @param string $filePath Full path to JSON file
* @return array|false Decoded array or false on failure
*
* @example $data = g::run("config.readJson", "/path/to/file.json");
*/
"readJson" => function ($filePath) {
if (!file_exists($filePath)) {
g::run("core.log", "[WARNING] File not found: $filePath", 2);
return false;
}
$json = file_get_contents($filePath);
if ($json === false) {
g::run("core.log", "[ERROR] Could not read file: $filePath", 1);
return false;
}
$data = json_decode($json, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
g::run("core.log", "[ERROR] Invalid JSON in file: $filePath - " . json_last_error_msg(), 1);
return false;
}
return $data;
},
/**
* Write data to JSON file
*
* Encodes data as JSON and writes to file.
* Creates directory if it doesn't exist.
*
* @param mixed $data Data to encode (array, object, etc.)
* @param string $filePath Full path to JSON file
* @param bool $pretty Whether to pretty-print JSON (default: true)
* @return bool Success status
*
* @example g::run("config.writeJson", $data, "/path/to/file.json");
*/
"writeJson" => function ($data, $filePath, $pretty = true) {
// Ensure directory exists
$dir = dirname($filePath);
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) {
g::run("core.log", "[ERROR] Could not create directory: $dir", 1);
return false;
}
}
// Encode data
$options = JSON_UNESCAPED_SLASHES;
if ($pretty) {
$options |= JSON_PRETTY_PRINT;
}
$json = json_encode($data, $options);
if ($json === false) {
g::run("core.log", "[ERROR] Could not encode JSON: " . json_last_error_msg(), 1);
return false;
}
// Write to file
$result = file_put_contents($filePath, $json);
if ($result === false) {
g::run("core.log", "[ERROR] Could not write to file: $filePath", 1);
return false;
}
g::run("core.log", "[CONFIG] Written to file: $filePath (" . strlen($json) . " bytes)", 3);
return true;
},
/**
* Update a specific configuration value
*
* Updates config in memory and optionally saves to file.
* Supports dot notation for nested keys.
*
* @param string $key Config key (supports dot notation: "database.host")
* @param mixed $value New value
* @param bool $save Whether to save to config file (default: true)
* @return bool Success status
*
* @example g::run("config.update", "database.host", "localhost", true);
* @example g::run("config.update", "settings.debug", true);
*/
"update" => function ($key, $value, $save = true) {
$config = g::get("config");
if (!$config) {
g::run("core.log", "[ERROR] Config not loaded", 1);
return false;
}
// Update in memory using dot notation
$keys = explode('.', $key);
$ref = &$config;
foreach ($keys as $k) {
if (!isset($ref[$k])) {
$ref[$k] = array();
}
$ref = &$ref[$k];
}
$ref = $value;
// Update stored config
g::set("config", $config);
// Also update in the nested location for backwards compatibility
g::set("config." . $key, $value);
// Save to file if requested
if ($save) {
return g::run("core.saveConfig", $config);
}
return true;
},
/**
* Load and merge an external config file
*
* Loads a JSON config file and merges it with existing config.
* Useful for environment-specific configs or plugin configs.
*
* @param string $filePath Path to config file
* @param bool $overwrite Whether external config overwrites existing (default: true)
* @return bool Success status
*
* @example g::run("config.load", "config.local.json");
* @example g::run("config.load", "plugins/my-plugin/config.json", false);
*/
"load" => function ($filePath, $overwrite = true) {
$externalConfig = g::run("config.readJson", $filePath);
if ($externalConfig === false) {
return false;
}
$currentConfig = g::get("config");
if ($overwrite) {
// External config takes precedence
$mergedConfig = g::run("core.arrayMergeRecursive", $currentConfig, $externalConfig);
} else {
// Current config takes precedence
$mergedConfig = g::run("core.arrayMergeRecursive", $externalConfig, $currentConfig);
}
g::set("config", $mergedConfig);
g::run("core.log", "[CONFIG] Loaded external config: $filePath", 3);
return true;
},
/**
* Get a specific configuration value with default fallback
*
* Retrieves a config value using dot notation.
* Returns default value if key doesn't exist.
*
* @param string $key Config key (dot notation: "database.host")
* @param mixed $default Default value if key not found
* @return mixed Config value or default
*
* @example $host = g::run("config.getValue", "database.host", "localhost");
* @example $debug = g::run("config.getValue", "settings.debug", false);
*/
"getValue" => function ($key, $default = null) {
$config = g::get("config");
if (!$config || !is_array($config)) {
return $default;
}
// Navigate through nested keys
$keys = explode('.', $key);
$value = $config;
foreach ($keys as $k) {
if (!is_array($value) || !array_key_exists($k, $value)) {
return $default;
}
$value = $value[$k];
}
return $value;
},
/**
* Check if a configuration key exists
*
* @param string $key Config key (dot notation)
* @return bool Whether the key exists
*
* @example if (g::run("config.has", "database.host")) { ... }
*/
"has" => function ($key) {
$config = g::get("config");
if (!$config || !is_array($config)) {
return false;
}
// Navigate through nested keys
$keys = explode('.', $key);
$value = $config;
foreach ($keys as $k) {
if (!is_array($value) || !array_key_exists($k, $value)) {
return false;
}
$value = $value[$k];
}
return true;
},
/**
* Reset configuration to defaults
*
* Useful for testing or resetting after errors.
* Optionally saves the reset config to file.
*
* @param bool $save Whether to save reset config to file
* @return bool Success status
*
* @example g::run("config.reset", true);
*/
"reset" => function ($save = false) {
// Trigger prepareConfig again to load defaults
g::run("core.prepareConfig");
if ($save) {
$config = g::get("config");
return g::run("core.saveConfig", $config);
}
g::run("core.log", "[CONFIG] Configuration reset to defaults", 2);
return true;
},
));
// ============================================================================
// PHASE 3: LOGGING & MESSAGING
// ============================================================================
g::def("log", array(
/**
* Write a message with importance level
*
* Logs to file and optionally stores in memory for display to user.
* Level determines if message is logged/shown based on config settings.
*
* Levels:
* 1 = ERROR - Critical errors
* 2 = WARNING - Important warnings
* 3 = INFO - Informational messages
* 4 = DEBUG - Debug information
*
* @param string $message The message to log
* @param int $level Message importance level (1-4)
* @param bool $display Whether to also store for user display (default: false)
* @return void
*
* @example g::run("log.write", "Database connected", 3);
* @example g::run("log.write", "Invalid login attempt", 2, true);
*/
"write" => function ($message, $level = 3, $display = false) {
$configLevel = g::run("config.getValue", "settings.log_level", 1);
// Only log if message level is <= configured level
if ($level <= $configLevel) {
if (!is_dir(DATA_FOLDER)) {
mkdir(DATA_FOLDER, 0755, true);
}
$timestamp = g::run("core.timestamp");
$levelName = g::run("log.getLevelName", $level);
$logEntry = "[$timestamp] [$levelName] $message\n";
file_put_contents(LOG_FILE, $logEntry, FILE_APPEND);
}
// Store for user display if requested
if ($display) {
g::run("log.addMessage", $message, $level);
}
},
/**
* Get level name from number
*
* @param int $level Level number (1-4)
* @return string Level name
*/
"getLevelName" => function ($level) {
$levels = array(
1 => "ERROR",
2 => "WARNING",
3 => "INFO",
4 => "DEBUG"
);
return isset($levels[$level]) ? $levels[$level] : "UNKNOWN";
},
/**
* Add a message for user display
*
* Stores messages in memory that can be retrieved and shown to users.
* Supports translation and categorization by level.
*
* @param string $message The message (can be a translation key)
* @param int $level Message level (1-4)
* @return void
*
* @example g::run("log.addMessage", "user.login.success", 3);
*/
"addMessage" => function ($message, $level = 3) {
$messages = g::get("messages");
if (!is_array($messages)) {
$messages = array();
}
$timestamp = g::run("core.timestamp");
$translated = g::run("log.translate", $message);
$messages[] = array(
"message" => $translated,
"original" => $message,
"level" => $level,
"time" => $timestamp
);
g::set("messages", $messages);
},
/**
* Get all stored messages
*
* Retrieves all messages stored for user display.
* Optionally filter by level.
*
* @param int|null $filterLevel Only return messages of this level (optional)
* @return array Array of message objects
*
* @example $messages = g::run("log.getMessages");
* @example $errors = g::run("log.getMessages", 1);
*/
"getMessages" => function ($filterLevel = null) {
$messages = g::get("messages");
if (!is_array($messages)) {
return array();
}
if ($filterLevel === null) {
return $messages;
}
// Filter by level
$filtered = array();
foreach ($messages as $msg) {
if ($msg["level"] === $filterLevel) {
$filtered[] = $msg;
}
}
return $filtered;
},
/**
* Clear all stored messages
*
* @return void
*/
"clearMessages" => function () {
g::set("messages", array());
},
/**
* Translate a message
*
* Looks up message in translation dictionary (bits).
* Falls back to original message if translation not found.
*
* Translation files should be loaded into g::set("translations.{lang}", array)
*
* @param string $key Translation key (e.g., "user.login.success")
* @return string Translated message or original key
*
* @example $msg = g::run("log.translate", "error.not.found");
*/
"translate" => function ($key) {
$lang = g::run("config.getValue", "settings.language", "en");
$translations = g::get("translations.$lang");
if ($translations && is_array($translations) && isset($translations[$key])) {
return $translations[$key];
}
// Try default language if current language fails
if ($lang !== "en") {
$translations = g::get("translations.en");
if ($translations && is_array($translations) && isset($translations[$key])) {
return $translations[$key];
}
}
// Return original key if no translation found
return $key;
},
/**
* Load translations from a JSON file
*
* Loads translation dictionary for a specific language.
* Translations should be in format: {"key": "value"}
*
* @param string $filePath Path to translation JSON file
* @param string $lang Language code (e.g., "en", "es", "fr")
* @return bool Success status
*
* @example g::run("log.loadTranslations", "data/lang/en.json", "en");
*/
"loadTranslations" => function ($filePath, $lang) {
$translations = g::run("config.readJson", $filePath);
if ($translations === false) {
g::run("log.write", "Could not load translations from: $filePath", 2);
return false;
}
g::set("translations.$lang", $translations);
g::run("log.write", "Loaded translations for language: $lang", 3);
return true;
},
/**
* Get performance metrics
*
* Returns execution time and memory usage since framework start.
* Useful for debugging and optimization.
*
* @param bool $formatted Return formatted string vs array (default: false)
* @return array|string Performance data
*
* @example $perf = g::run("log.performance");
* @example echo g::run("log.performance", true);
*/
"performance" => function ($formatted = false) {
$startTime = g::get("system.time.start");
$startMemory = g::get("system.memory.start");
if (!$startTime) {
$startTime = microtime(true);
}
if (!$startMemory) {
$startMemory = 0;
}
$endTime = microtime(true);
$endMemory = memory_get_usage();
$executionTime = round(($endTime - $startTime) * 1000, 2); // milliseconds
$memoryUsed = round(($endMemory - $startMemory) / 1024, 2); // kilobytes
$peakMemory = round(memory_get_peak_usage() / 1024, 2); // kilobytes
$data = array(
"execution_time_ms" => $executionTime,
"memory_used_kb" => $memoryUsed,
"peak_memory_kb" => $peakMemory,
"start_time" => $startTime,
"end_time" => $endTime
);
if ($formatted) {
return "Execution: {$executionTime}ms | Memory: {$memoryUsed}KB | Peak: {$peakMemory}KB";
}
return $data;
},
/**
* Debug print - enhanced output for development
*
* Pretty-prints data with optional label and HTML formatting.
* Only outputs if debug mode is enabled.
*
* @param mixed $data Data to print
* @param string $label Optional label for the output
* @param bool $return Return string instead of echo (default: false)
* @return string|void Output string if $return is true
*
* @example g::run("log.debugPrint", $userData, "User Object");
* @example g::run("log.debugPrint", $config);
*/
"debugPrint" => function ($data, $label = "", $return = false) {
$debug = g::run("config.getValue", "settings.debug", false);
if (!$debug) {
return $return ? "" : null;
}
$output = "";
if ($label) {
$output .= "DEBUG: $label\n";
} else {
$output .= "DEBUG:\n";
}
$output .= "";
if (is_array($data) || is_object($data)) {
$output .= print_r($data, true);
} else {
$output .= var_export($data, true);
}
$output .= "\n";
if ($return) {
return $output;
}
echo $output;
},
/**
* Quick logging shortcuts for common levels
*/
"error" => function ($message, $display = true) {
return g::run("log.write", $message, 1, $display);
},
"warning" => function ($message, $display = false) {
return g::run("log.write", $message, 2, $display);
},
"info" => function ($message, $display = false) {
return g::run("log.write", $message, 3, $display);
},
"debugLog" => function ($message) {
return g::run("log.write", $message, 4, false);
},
));
// ============================================================================
// PHASE 4: ROUTING & URL PARSING
// ============================================================================
g::def("route", array(
/**
* Parse the current request URL
*
* Extracts and parses the current request URL into components:
* - protocol (http/https)
* - host (domain)
* - path (URI path)
* - query (query string)
* - segments (path split into array)
*
* Stores result in g::set("request.*")
*
* @return array Parsed URL components
*
* @example $url = g::run("route.parseUrl");
* @example // Returns: ["protocol" => "https", "host" => "example.com", "path" => "/blog/post", ...]
*/
"parseUrl" => function () {
// Get protocol
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
// Get host
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
// Get request URI (path + query string)
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
// CRITICAL: Split path and query string FIRST (support both ? and ; delimiters)
// This must happen before ANY other processing to prevent query params from contaminating the path
$path = $requestUri;
$queryString = '';
// Check for question mark first (most common), then semicolon
$queryPos = strpos($requestUri, '?');
$semiPos = strpos($requestUri, ';');
if ($queryPos !== false || $semiPos !== false) {
// Use whichever comes first
$splitPos = false;
$splitChar = '';
if ($queryPos !== false && ($semiPos === false || $queryPos < $semiPos)) {
$splitPos = $queryPos;
$splitChar = '?';
} elseif ($semiPos !== false) {
$splitPos = $semiPos;
$splitChar = ';';
}
if ($splitPos !== false) {
$path = substr($requestUri, 0, $splitPos);
$queryString = substr($requestUri, $splitPos + 1);
}
}
// Auto-detect and remove base path (for apps in subfolders)
// Extract the actual web-accessible base path by comparing SCRIPT_NAME with REQUEST_URI
$scriptName = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';
$requestPath = $path; // This is the path from REQUEST_URI
// Get the directory name from SCRIPT_NAME (e.g., /dv-private/genes_one/index.php -> genes_one)
$scriptDir = dirname($scriptName);
$scriptDirName = basename($scriptDir);
// Find where this directory name appears in the REQUEST_URI
$basePath = '';
if ($scriptDirName && strpos($requestPath, $scriptDirName) !== false) {
// Find the position of the script directory in the request path
$pos = strpos($requestPath, '/' . $scriptDirName);
if ($pos !== false) {
// Extract everything up to and including the directory name as base path
$basePath = substr($requestPath, 0, $pos + strlen($scriptDirName) + 1);
$basePath = rtrim($basePath, '/');
}
}
// Strip base path from the beginning of the path
if ($basePath && strpos($path, $basePath) === 0) {
$path = substr($path, strlen($basePath));
if (empty($path) || $path[0] !== '/') {
$path = '/' . $path;
}
}
// Ensure path has leading slash
if (empty($path)) {
$path = '/';
} elseif ($path[0] !== '/') {
$path = '/' . $path;
}
// Parse query string into array
$query = array();
if ($queryString) {
parse_str($queryString, $query);
}
// Split path into segments (remove empty segments)
$segments = array_values(array_filter(explode('/', trim($path, '/'))));
// Get request method
$method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
// Get IP address
$ip = g::run("route.getClientIp");
$urlData = array(
"protocol" => $protocol,
"host" => $host,
"path" => $path,
"query_string" => $queryString,
"query" => $query,
"segments" => $segments,
"method" => $method,
"ip" => $ip,
"full_url" => $protocol . '://' . $host . $requestUri
);
// Store for later use
g::set("request", $urlData);
return $urlData;
},
/**
* Get client IP address
*
* Tries to determine real client IP even behind proxies.
*
* @return string Client IP address
*/
"getClientIp" => function () {
$ipKeys = array(
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
);
foreach ($ipKeys as $key) {
if (isset($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
$ip = trim($ips[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return 'unknown';
},
/**
* Match a URL pattern to current request
*
* Supports wildcards and parameters:
* - "/blog" - Exact match
* - "/blog/*" - Match /blog/anything
* - "/blog/:id" - Match /blog/123 and capture id=123
* - "/blog/:id/edit" - Capture parameter in middle
*
* @param string $pattern URL pattern to match
* @param array|null $request Request data (optional, uses current if null)
* @return array|false Matched parameters or false
*
* @example $params = g::run("route.match", "/blog/:id");
* @example if ($params) { $id = $params['id']; }
*/
"match" => function ($pattern, $request = null) {
if ($request === null) {
$request = g::get("request");
}
if (!$request || !isset($request['path'])) {
return false;
}
$path = $request['path'];
// Normalize pattern and path
$pattern = '/' . trim($pattern, '/');
$path = '/' . trim($path, '/');
// Exact match (easiest case)
if ($pattern === $path) {
return array();
}
// Convert pattern to regex
$regex = $pattern;
// Replace :param with named capture groups
$regex = preg_replace_callback('/:([a-zA-Z0-9_]+)/', function ($matches) {
return '(?P<' . $matches[1] . '>[^/]+)';
}, $regex);
// Replace * with wildcard
$regex = str_replace('*', '.*', $regex);
// Add anchors
$regex = '#^' . $regex . '$#';
// Try to match
if (preg_match($regex, $path, $matches)) {
// Extract only named captures
$params = array();
foreach ($matches as $key => $value) {
if (!is_numeric($key)) {
$params[$key] = $value;
}
}
return $params;
}
return false;
},
/**
* Define a route with handler
*
* Associates a URL pattern with a handler function.
* Routes are stored and can be dispatched later.
*
* @param string $pattern URL pattern (e.g., "/blog/:id")
* @param callable $handler Function to call if route matches
* @param string $method HTTP method (GET, POST, etc.) or * for all
* @return void
*
* @example g::run("route.define", "/blog/:id", function($params) {
* echo "Blog post: " . $params['id'];
* });
*/
"define" => function ($pattern, $handler, $method = '*') {
$routes = g::get("routes");
if (!is_array($routes)) {
$routes = array();
}
$method = strtoupper($method);
if (!isset($routes[$method])) {
$routes[$method] = array();
}
$routes[$method][] = array(
"pattern" => $pattern,
"handler" => $handler
);
g::set("routes", $routes);
},
/**
* Dispatch - find and execute matching route
*
* Tries to match current request against defined routes.
* Executes the first matching route's handler.
*
* @param array|null $request Request data (optional)
* @return mixed Result of handler function, or false if no match
*
* @example g::run("route.dispatch");
*/
"dispatch" => function ($request = null) {
if ($request === null) {
$request = g::get("request");
}
if (!$request) {
g::run("log.warning", "No request data for routing");
return false;
}
$routes = g::get("routes");
if (!is_array($routes) || empty($routes)) {
g::run("log.warning", "No routes defined");
return false;
}
$method = $request['method'];
// Check method-specific routes first, then * (wildcard) routes
$routesToCheck = array();
if (isset($routes[$method])) {
$routesToCheck = array_merge($routesToCheck, $routes[$method]);
}
if (isset($routes['*'])) {
$routesToCheck = array_merge($routesToCheck, $routes['*']);
}
// Try to match routes
foreach ($routesToCheck as $route) {
$params = g::run("route.match", $route['pattern'], $request);
if ($params !== false) {
// Match found - merge params with query params
$allParams = array_merge($request['query'], $params);
// Execute handler
if (is_callable($route['handler'])) {
g::run("log.info", "Route matched: {$route['pattern']}");
return call_user_func($route['handler'], $allParams, $request);
}
return true;
}
}
// No match found
g::run("log.info", "No route matched for: {$request['path']}");
return false;
},
/**
* Build a URL from pattern and parameters
*
* Replaces parameters in pattern with actual values.
* Uses semicolon-delimited query parameters instead of ? and &
*
* @param string $pattern URL pattern with :params
* @param array $params Parameter values
* @param array $query Query string parameters (optional)
* @return string Built URL
*
* @example $url = g::run("route.build", "/blog/:id", ["id" => 123]);
* @example // Returns: "/blog/123"
*
* @example $url = g::run("route.build", "/blog/:id", ["id" => 123], ["page" => 2]);
* @example // Returns: "/blog/123;page=2"
*/
"build" => function ($pattern, $params = array(), $query = array()) {
$url = $pattern;
// Replace :param with values
foreach ($params as $key => $value) {
$url = str_replace(':' . $key, $value, $url);
}
// Add query string with semicolon delimiters
if (!empty($query)) {
$queryParts = array();
foreach ($query as $key => $value) {
$queryParts[] = $key . '=' . urlencode($value);
}
$url .= ';' . implode(';', $queryParts);
}
return $url;
},
/**
* Redirect to a URL
*
* Sends HTTP redirect header and optionally exits.
*
* @param string $url URL to redirect to
* @param int $statusCode HTTP status code (301, 302, etc.)
* @param bool $exit Whether to exit after redirect
* @return void
*
* @example g::run("route.redirect", "/login");
* @example g::run("route.redirect", "/blog/123", 301);
*/
"redirect" => function ($url, $statusCode = 302, $exit = true) {
// Ensure no output has been sent
if (headers_sent()) {
g::run("log.error", "Cannot redirect - headers already sent");
echo "";
return;
}
// Set status code
http_response_code($statusCode);
// Send location header
header("Location: $url");
g::run("log.info", "Redirecting to: $url");
if ($exit) {
exit;
}
},
/**
* Define route aliases
*
* Maps multiple URL patterns to the same handler.
* Useful for multilingual URLs or alternate paths.
*
* @param array $patterns Array of URL patterns
* @param callable $handler Function to call for all patterns
* @param string $method HTTP method (GET, POST, etc.) or * for all
* @return void
*
* @example g::run("route.alias", array("/membership", "/uyelik", "/mitgliedschaft"), function($params) {
* echo "Membership page - Lang: " . $params['_lang'];
* });
*/
"alias" => function ($patterns, $handler, $method = '*') {
if (!is_array($patterns)) {
$patterns = array($patterns);
}
foreach ($patterns as $pattern) {
g::run("route.define", $pattern, $handler, $method);
}
g::run("log.debug", "Registered " . count($patterns) . " route aliases");
},
/**
* Define translated routes
*
* Maps localized URLs to handler with language context.
* Automatically passes language code to handler.
*
* @param array $translations Associative array: lang => pattern
* @param callable $handler Function to call (receives $params with '_lang' key)
* @param string $method HTTP method
* @return void
*
* @example g::run("route.translate", array(
* "en" => "/membership",
* "tr" => "/uyelik",
* "de" => "/mitgliedschaft"
* ), function($params) {
* echo "Language: " . $params['_lang'];
* });
*/
"translate" => function ($translations, $handler, $method = '*') {
if (!is_array($translations)) {
g::run("log.error", "route.translate expects array of translations");
return;
}
foreach ($translations as $lang => $pattern) {
// Wrap handler to inject language
$langHandler = function ($params, $request = null) use ($handler, $lang) {
$params['_lang'] = $lang;
return call_user_func($handler, $params, $request);
};
g::run("route.define", $pattern, $langHandler, $method);
}
g::run("log.debug", "Registered translated routes for " . count($translations) . " languages");
},
/**
* Resolve route with fallback chain
*
* Smart resolution: tries mod routes ā clone functions ā default handler ā 404
* This is the main dispatcher that checks multiple sources.
*
* @param array|null $request Request data (optional)
* @param callable|null $notFoundHandler Custom 404 handler
* @return mixed Result of handler or false
*
* @example g::run("route.resolve");
*
* @example g::run("route.resolve", null, function() {
* echo "404 - Page Not Found";
* });
*/
"resolve" => function ($request = null, $notFoundHandler = null) {
if ($request === null) {
$request = g::get("request");
}
if (!$request) {
g::run("log.warning", "No request data for route resolution");
return false;
}
$path = $request['path'];
g::run("log.debug", "Resolving route: $path");
// STEP 1: Try mod routes first (from Phase 8)
$modRoutes = g::get("mods.routes");
if ($modRoutes && isset($modRoutes[$path])) {
$modRoute = $modRoutes[$path];
g::run("log.info", "Route resolved to mod: {$modRoute['mod']}");
$handler = $modRoute['handler'];
if (g::has($handler)) {
return g::run($handler, array_merge($request['query'], array('_request' => $request)));
}
}
// STEP 2: Try standard route dispatch (includes translated routes, aliases, etc.)
$result = g::run("route.dispatch", $request);
if ($result !== false) {
return $result;
}
// STEP 3: Try clone-defined functions
// Clone functions should be named like "clone.home", "clone.about", etc.
$pathClean = trim($path, '/');
if (empty($pathClean)) {
$pathClean = 'index';
}
// Try: clone.{path}
$cloneFunction = "clone." . str_replace('/', '.', $pathClean);
if (g::has($cloneFunction)) {
g::run("log.info", "Route resolved to clone function: $cloneFunction");
return g::run($cloneFunction, array_merge($request['query'], array('_request' => $request)));
}
// STEP 4: Try default handlers (from route.defaults)
$defaults = g::get("route.defaults");
if ($defaults && isset($defaults[$path])) {
g::run("log.info", "Route resolved to default handler");
$handler = $defaults[$path];
if (is_callable($handler)) {
return call_user_func($handler, $request['query'], $request);
}
}
// STEP 5: 404 - Not Found
g::run("log.info", "Route not found: $path");
if ($notFoundHandler && is_callable($notFoundHandler)) {
return call_user_func($notFoundHandler, $request);
}
// Try registered 404 handler
$default404 = g::get("route.404");
if ($default404 && is_callable($default404)) {
return call_user_func($default404, $request);
}
// Default 404 response
http_response_code(404);
echo "404 - Page Not Found: $path";
return false;
},
/**
* Set default route handlers
*
* Define handlers for common routes like /, /index, 404, etc.
* These are fallback handlers used when no other route matches.
*
* @param array $defaults Associative array: path => handler
* @return void
*
* @example g::run("route.defaults", array(
* "/" => function() { echo "Home"; },
* "/index" => function() { echo "Home"; },
* "404" => function() { echo "Not Found"; }
* ));
*/
"defaults" => function ($defaults) {
if (!is_array($defaults)) {
g::run("log.error", "route.defaults expects array");
return;
}
$existing = g::get("route.defaults");
if (!is_array($existing)) {
$existing = array();
}
// Handle special 404 handler
if (isset($defaults['404'])) {
g::set("route.404", $defaults['404']);
unset($defaults['404']);
}
// Merge with existing defaults
$merged = array_merge($existing, $defaults);
g::set("route.defaults", $merged);
g::run("log.debug", "Registered " . count($defaults) . " default route handlers");
},
/**
* Handle routing with views from config.json
* Detects language, matches route to view, runs mapped function
* Automatically handles API routes (/api/*) through api.handle
*/
"handle" => function () {
$config = g::get("config");
// ALWAYS parse the URL fresh to ensure query strings are properly stripped
// This prevents issues with UTM parameters and other query strings breaking routing
$request = g::run("route.parseUrl");
$path = $request['path'];
$isJson = false;
// Remove trailing slash for consistency
$path = rtrim($path, '/');
// ============================================================
// API ROUTE DETECTION - Auto-handle /api/* requests
// ============================================================
$pathSegments = array_values(array_filter(explode('/', trim($path, '/'))));
if (isset($pathSegments[0]) && $pathSegments[0] === 'api') {
// API request detected - send JSON response
header('Content-Type: application/json');
// Build API path (remove 'api' prefix)
$apiSegments = array_slice($pathSegments, 1);
$apiPath = '/' . implode('/', $apiSegments);
// Get request method
$method = isset($request['method']) ? $request['method'] : 'GET';
// Get request data
$data = null;
if ($method === 'GET') {
$data = $request['query'];
} elseif (in_array($method, array('POST', 'PUT', 'PATCH', 'DELETE'))) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if ($data === null) {
$data = $_POST;
}
}
// Handle the API request
$response = g::run("api.handle", $apiPath, $method, $data);
echo json_encode($response);
exit; // Stop further processing
}
// ============================================================
// Check for AJAX request (for client-side router)
if (isset($_GET['ajax']) || isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
g::set("response.is_ajax", true);
}
// Check for .json suffix BEFORE extracting segments
if (substr($path, -5) === '.json') {
$isJson = true;
$path = substr($path, 0, -5);
g::set("response.format", "json");
}
// Clean path and split into segments
$path = trim($path, '/');
$pathSegments = !empty($path) ? explode('/', $path) : array();
// DEFENSIVE: Strip query parameters from each segment (in case they leaked through)
// This prevents URLs like /s/hash?utm_source=chatgpt from breaking the routing
$pathSegments = array_map(function($segment) {
// Remove everything after ? or ; from each segment
if (strpos($segment, '?') !== false) {
$segment = substr($segment, 0, strpos($segment, '?'));
}
if (strpos($segment, ';') !== false) {
$segment = substr($segment, 0, strpos($segment, ';'));
}
return $segment;
}, $pathSegments);
// First segment is the route, rest are additional route segments
// If empty path, default to 'index'
if (empty($pathSegments)) {
$routePath = 'index';
$additionalSegments = array();
} else {
$routePath = $pathSegments[0];
$additionalSegments = array_slice($pathSegments, 1);
}
// Store additional segments in request for view functions to use
$request['route_segments'] = $additionalSegments;
g::set("request", $request);
// Get current domain and domain config
$currentDomain = g::get("current_domain");
// Strip domain identifier subdirectory from path if present
// (e.g., /rockers_im/album/... becomes /album/...)
$domainRules = isset($config["domain_rules"]) ? $config["domain_rules"] : array();
$host = isset($_SERVER['HTTP_HOST']) ? preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']) : 'localhost';
$domainBasePath = ''; // Store the subdirectory that was stripped
// Check if first segment matches a domain identifier
if (!empty($pathSegments)) {
$firstSegment = $pathSegments[0];
$checkIdentifier = $host . '/' . $firstSegment;
// If this matches a domain rule, strip it
foreach ($domainRules as $pattern => $domainKey) {
if ($pattern === $checkIdentifier && $domainKey === $currentDomain) {
// Store the base path before removing it
$domainBasePath = '/' . $firstSegment;
// Remove first segment (domain identifier)
$pathSegments = array_slice($pathSegments, 1);
// Update route path and segments
if (empty($pathSegments)) {
$routePath = 'index';
$additionalSegments = array();
} else {
$routePath = $pathSegments[0];
$additionalSegments = array_slice($pathSegments, 1);
}
// Update request with base path
$request['route_segments'] = $additionalSegments;
$request['domain_base_path'] = $domainBasePath;
g::set("request", $request);
break;
}
}
}
// Get base views from config
$views = isset($config["views"]) ? $config["views"] : array();
$bits = isset($config["bits"]) ? $config["bits"] : array();
// Get domain-specific configuration
$domainConfig = isset($config["domains"][$currentDomain])
? $config["domains"][$currentDomain]
: array();
// Merge domain-specific view overrides
if (isset($domainConfig["views_override"])) {
$views = array_merge($views, $domainConfig["views_override"]);
}
// Store domain config for later use in views
g::set("domain_config", $domainConfig);
// Find matching view and detect language from URL
$matchedView = null;
$viewName = null;
$lang = null;
foreach ($views as $vName => $viewConfig) {
$urls = isset($viewConfig["urls"]) ? $viewConfig["urls"] : array();
foreach ($urls as $l => $url) {
// Match if routePath equals URL
if ($routePath === $url) {
$matchedView = $viewConfig;
$viewName = $vName;
$lang = $l; // Set language based on which URL matched
break 2;
}
}
}
// If no match found, immediately return 404
if (!$matchedView) {
http_response_code(404);
if (g::has("clone.NotFound")) {
$lang = g::run("route.detectLanguage");
return g::run("clone.NotFound", array(), $lang, $path);
}
echo "404 - Page Not Found";
exit;
}
// Fallback to auto-detect if no language was set from URL
if (!$lang) {
$lang = g::run("route.detectLanguage");
}
if ($matchedView) {
$function = isset($matchedView["function"]) ? $matchedView["function"] : "clone.Index";
$viewBits = isset($matchedView["bits"]) ? $matchedView["bits"] : array();
// Translate bits for current language
$translatedBits = g::run("route.translateBits", $viewBits, $bits, $lang);
// Run the function
if (g::has($function)) {
// If JSON format requested, capture output and return data
if ($isJson) {
ob_start();
$result = g::run($function, $translatedBits, $lang, $path);
$output = ob_get_clean();
// Prepare JSON response with system info
$jsonData = array(
"success" => true,
"view" => $viewName,
"lang" => $lang,
"path" => $path,
"request" => array(
"method" => $request["method"],
"query" => $request["query"],
"segments" => $request["segments"],
"route_segments" => isset($request["route_segments"]) ? $request["route_segments"] : array(),
"ip" => $request["ip"]
)
);
// Add authentication info
$user = g::run("auth.user");
if ($user) {
$jsonData["auth"] = array(
"authenticated" => true,
"user" => $user
);
} else {
$jsonData["auth"] = array(
"authenticated" => false,
"user" => null
);
}
// If function returned data, use it
if ($result !== null && $result !== '') {
if (is_array($result)) {
$jsonData = array_merge($jsonData, $result);
} else {
$jsonData["data"] = $result;
}
}
// If there was output, include it
else if (!empty($output)) {
$jsonData["html"] = $output;
}
header('Content-Type: application/json');
echo json_encode($jsonData);
return $jsonData;
} else {
// Normal HTML response
$result = g::run($function, $translatedBits, $lang, $path);
return $result;
}
} else {
g::run("log.error", "View function not found: $function");
// Show helpful error in debug mode
if (g::get("config.settings.debug")) {
echo "Error: Function Not Found
";
echo "The function $function is not defined.
";
echo "Make sure you have g::def('clone', array(...)) in your index.php
";
}
}
} // Close if ($matchedView)
},
/**
* Detect language from URL, session, or config
*/
"detectLanguage" => function () {
$config = g::get("config");
$defaultLang = isset($config["clone"]["default_language"]) ? $config["clone"]["default_language"] : "en";
// Check session first
if (isset($_SESSION["language"])) {
return $_SESSION["language"];
}
// Check cookie
if (isset($_COOKIE["language"])) {
return $_COOKIE["language"];
}
// Check Accept-Language header
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$acceptLang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
$languages = isset($config["clone"]["languages"]) ? $config["clone"]["languages"] : array("en");
if (in_array($acceptLang, $languages)) {
return $acceptLang;
}
}
return $defaultLang;
},
/**
* Translate bits to current language
*/
"translateBits" => function ($viewBits, $globalBits, $lang) {
$translated = array();
// Merge global bits first
foreach ($globalBits as $key => $translations) {
if (is_array($translations) && isset($translations[$lang])) {
$translated[$key] = $translations[$lang];
} else if (isset($translations["en"])) {
$translated[$key] = $translations["en"]; // Fallback to English
} else {
$translated[$key] = $key;
}
}
// Then view-specific bits (overrides global)
foreach ($viewBits as $key => $translations) {
if (is_array($translations) && isset($translations[$lang])) {
$translated[$key] = $translations[$lang];
} else if (isset($translations["en"])) {
$translated[$key] = $translations["en"];
} else {
$translated[$key] = $key;
}
}
return $translated;
},
));
// ============================================================================
// PHASE 5: AUTHENTICATION & SESSION MANAGEMENT
// ============================================================================
// Combined authentication system moved to PHASE 7.75 (auth.* namespace)
// Supports BOTH file-based (users.json) AND database-based authentication
// ============================================================================
// PHASE 6: CRYPTOGRAPHY
// ============================================================================
g::def("crypt", array(
/**
* Hash a password using bcrypt with application pepper
*
* Uses password_hash with additional HMAC pepper from config.security.secret
* This adds an application-wide secret layer on top of bcrypt's per-password salt.
*
* @param string $password Plain text password
* @return string Hashed password
*
* @example $hash = g::run("crypt.hashPassword", "secret123");
*/
"hashPassword" => function ($password) {
// Get application secret for peppering
$config = g::get("config");
$pepper = isset($config['security']['secret']) ? $config['security']['secret'] : '';
// Apply pepper using HMAC before hashing
// This means even if DB is compromised, attacker needs config.json secret
$pepperedPassword = hash_hmac('sha256', $password, $pepper);
// Now hash with bcrypt (which adds its own random salt)
return password_hash($pepperedPassword, PASSWORD_DEFAULT);
},
/**
* Verify a password against a peppered hash
*
* Applies same HMAC pepper from config.security.secret before verifying.
*
* @param string $password Plain text password
* @param string $hash Hashed password
* @return bool Match status
*
* @example if (g::run("crypt.verifyPassword", $input, $hash)) { }
*/
"verifyPassword" => function ($password, $hash) {
// Get application secret for peppering
$config = g::get("config");
$pepper = isset($config['security']['secret']) ? $config['security']['secret'] : '';
// Apply same pepper transformation
$pepperedPassword = hash_hmac('sha256', $password, $pepper);
// Verify against bcrypt hash
return password_verify($pepperedPassword, $hash);
},
/**
* Generate a cryptographically secure token
*
* Useful for API keys, password reset tokens, CSRF tokens, etc.
*
* @param int $length Token length in bytes (default: 32)
* @return string Hex token
*
* @example $token = g::run("crypt.token");
* @example $apiKey = g::run("crypt.token", 64);
*/
"token" => function ($length = 32) {
if (function_exists('random_bytes')) {
return bin2hex(random_bytes($length));
} elseif (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(openssl_random_pseudo_bytes($length));
} else {
// Fallback for older PHP
return g::run("core.generateRandomKey", $length * 2, "hex");
}
},
/**
* Generate a hash of data
*
* @param string $data Data to hash
* @param string $algo Hash algorithm (default: sha256)
* @return string Hash string
*
* @example $hash = g::run("crypt.hash", "data");
* @example $md5 = g::run("crypt.hash", "data", "md5");
*/
"hash" => function ($data, $algo = 'sha256') {
return hash($algo, $data);
},
/**
* Create a keyed hash (HMAC)
*
* Used for message authentication and API signatures.
*
* @param string $data Data to hash
* @param string $key Secret key
* @param string $algo Hash algorithm (default: sha256)
* @return string HMAC hash
*
* @example $signature = g::run("crypt.hmac", $data, $secret);
*/
"hmac" => function ($data, $key, $algo = 'sha256') {
return hash_hmac($algo, $data, $key);
},
/**
* Encrypt data using AES-256-CBC
*
* Uses the secret from config.json as encryption key.
* Returns base64-encoded encrypted data with IV prepended.
*
* @param string $data Data to encrypt
* @param string|null $key Encryption key (uses config secret if null)
* @return string|false Encrypted data (base64) or false on failure
*
* @example $encrypted = g::run("crypt.encrypt", "secret data");
* @example $encrypted = g::run("crypt.encrypt", "data", "custom-key");
*/
"encrypt" => function ($data, $key = null) {
if (!function_exists('openssl_encrypt')) {
g::run("log.error", "OpenSSL not available for encryption");
return false;
}
// Use config secret if no key provided
if ($key === null) {
$config = g::run("config.get");
$key = isset($config['security']['secret']) ? $config['security']['secret'] : 'genes-default-key';
}
// Generate a key from the provided key
$encryptionKey = hash('sha256', $key, true);
// Generate random IV
$ivLength = openssl_cipher_iv_length('AES-256-CBC');
$iv = openssl_random_pseudo_bytes($ivLength);
// Encrypt the data
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
g::run("log.error", "Encryption failed");
return false;
}
// Prepend IV to encrypted data and encode
return base64_encode($iv . $encrypted);
},
/**
* Decrypt data encrypted with crypt.encrypt
*
* @param string $encryptedData Base64-encoded encrypted data
* @param string|null $key Encryption key (uses config secret if null)
* @return string|false Decrypted data or false on failure
*
* @example $decrypted = g::run("crypt.decrypt", $encrypted);
*/
"decrypt" => function ($encryptedData, $key = null) {
if (!function_exists('openssl_decrypt')) {
g::run("log.error", "OpenSSL not available for decryption");
return false;
}
// Use config secret if no key provided
if ($key === null) {
$config = g::run("config.get");
$key = isset($config['security']['secret']) ? $config['security']['secret'] : 'genes-default-key';
}
// Generate a key from the provided key
$encryptionKey = hash('sha256', $key, true);
// Decode the data
$data = base64_decode($encryptedData);
if ($data === false) {
g::run("log.error", "Invalid base64 data for decryption");
return false;
}
// Extract IV
$ivLength = openssl_cipher_iv_length('AES-256-CBC');
$iv = substr($data, 0, $ivLength);
$encrypted = substr($data, $ivLength);
// Decrypt
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
if ($decrypted === false) {
g::run("log.error", "Decryption failed");
return false;
}
return $decrypted;
},
/**
* Generate a UUID v4
*
* Useful for unique identifiers.
*
* @return string UUID string
*
* @example $uuid = g::run("crypt.uuid");
* @example // Returns: "550e8400-e29b-41d4-a716-446655440000"
*/
"uuid" => function () {
if (function_exists('random_bytes')) {
$data = random_bytes(16);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$data = openssl_random_pseudo_bytes(16);
} else {
// Fallback
$data = '';
for ($i = 0; $i < 16; $i++) {
$data .= chr(mt_rand(0, 255));
}
}
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
},
));
// ============================================================================
// PHASE 7: DATABASE ABSTRACTION
// ============================================================================
g::def("db", array(
/**
* Generate a unique hash ID from timestamp
*
* Creates a hash-based ID using encrypted timestamp with microseconds.
* IDs are chronologically sortable and unique.
*
* @return string Hash ID (32 chars)
*
* @example $hash = g::run("db.generateHash");
* @example // Returns: "a7f3k9m2p8q1r5t8w2x6z9c4"
*/
"generateHash" => function () {
// Get timestamp with microseconds
$timestamp = microtime(true);
// Create unique string from timestamp + random component
$unique = $timestamp . '.' . mt_rand(100000, 999999);
// Get salt from config for encryption
$config = g::run("config.get");
$salt = isset($config['security']['salt']) ? $config['security']['salt'] : 'genes-salt';
// Generate hash
$hash = hash('sha256', $unique . $salt);
// Return first 32 characters
return substr($hash, 0, 32);
},
/**
* Connect to a database
*
* Supports MySQL, SQLite, PostgreSQL.
* Connections are stored and reused.
*
* @param array $config Connection config
* @return bool|PDO Connection object or false
*
* @example g::run("db.connect", array(
* "driver" => "mysql",
* "name" => "main",
* "host" => "localhost",
* "database" => "mydb",
* "username" => "root",
* "password" => ""
* ));
*
* @example g::run("db.connect", array(
* "driver" => "sqlite",
* "name" => "users",
* "database" => "data/users.db"
* ));
*/
"connect" => function ($config) {
$driver = isset($config['driver']) ? $config['driver'] : 'mysql';
$name = isset($config['name']) ? $config['name'] : 'default';
// Check if already connected
$existing = g::get("db.connections.$name");
if ($existing) {
return $existing;
}
try {
$pdo = null;
if ($driver === 'mysql') {
$host = isset($config['host']) ? $config['host'] : 'localhost';
$port = isset($config['port']) ? $config['port'] : 3306;
$database = isset($config['database']) ? $config['database'] : '';
$username = isset($config['username']) ? $config['username'] : 'root';
$password = isset($config['password']) ? $config['password'] : '';
$charset = isset($config['charset']) ? $config['charset'] : 'utf8mb4';
$dsn = "mysql:host=$host;port=$port;dbname=$database;charset=$charset";
$pdo = new PDO($dsn, $username, $password, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
));
} elseif ($driver === 'sqlite') {
$database = isset($config['database']) ? $config['database'] : 'data/database.db';
// Create directory if needed
$dir = dirname($database);
if (!file_exists($dir)) {
mkdir($dir, 0755, true);
}
$dsn = "sqlite:$database";
$pdo = new PDO($dsn, null, null, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
));
// Enable JSON1 extension if available
$pdo->exec("PRAGMA foreign_keys = ON");
} elseif ($driver === 'pgsql' || $driver === 'postgresql') {
$host = isset($config['host']) ? $config['host'] : 'localhost';
$port = isset($config['port']) ? $config['port'] : 5432;
$database = isset($config['database']) ? $config['database'] : '';
$username = isset($config['username']) ? $config['username'] : 'postgres';
$password = isset($config['password']) ? $config['password'] : '';
$dsn = "pgsql:host=$host;port=$port;dbname=$database";
$pdo = new PDO($dsn, $username, $password, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
));
}
if ($pdo) {
// Store connection
g::set("db.connections.$name", $pdo);
g::set("db.config.$name", $config);
g::run("log.info", "Database connected: $name ($driver)");
return $pdo;
}
} catch (PDOException $e) {
g::run("log.error", "Database connection failed: " . $e->getMessage());
return false;
}
return false;
},
/**
* Get a database connection
*
* @param string $name Connection name (default: 'default')
* @return PDO|false Connection object or false
*
* @example $db = g::run("db.connection", "main");
*/
"connection" => function ($name = 'default') {
return g::get("db.connections.$name");
},
/**
* Execute a raw SQL query
*
* @param string $sql SQL query
* @param array $params Query parameters
* @param string $connection Connection name
* @return array|false Query results or false
*
* @example $results = g::run("db.query", "SELECT * FROM items WHERE type = ?", array("post"));
*/
"query" => function ($sql, $params = array(), $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
g::run("log.error", "No database connection: $connection");
return false;
}
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
// Check if it's a SELECT query
if (stripos(trim($sql), 'SELECT') === 0) {
return $stmt->fetchAll();
}
return true;
} catch (PDOException $e) {
g::run("log.error", "Query failed: " . $e->getMessage());
return false;
}
},
/**
* Execute multiple queries in a transaction
*
* @param callable $callback Function that receives PDO connection
* @param string $connection Connection name
* @return bool Success status
*
* @example g::run("db.transaction", function($db) {
* $db->exec("INSERT INTO items ...");
* $db->exec("INSERT INTO events ...");
* });
*/
"transaction" => function ($callback, $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
try {
$pdo->beginTransaction();
$result = call_user_func($callback, $pdo);
$pdo->commit();
g::run("log.info", "Transaction committed");
return $result !== false;
} catch (Exception $e) {
$pdo->rollBack();
g::run("log.error", "Transaction failed: " . $e->getMessage());
return false;
}
},
/**
* Insert a record
*
* Automatically generates hash if not provided.
* Adds created_at, updated_at timestamps.
*
* @param string $table Table name
* @param array $data Data to insert
* @param string $connection Connection name
* @return string|false Hash of inserted record or false
*
* @example $hash = g::run("db.insert", "items", array(
* "title" => "My Post",
* "type" => "post",
* "state" => "published"
* ));
*/
"insert" => function ($table, $data, $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
// Generate hash if not provided
if (!isset($data['hash'])) {
$data['hash'] = g::run("db.generateHash");
}
// Add timestamps
$now = g::run("core.now");
if (!isset($data['created_at'])) {
$data['created_at'] = $now;
}
if (!isset($data['updated_at'])) {
$data['updated_at'] = $now;
}
// Add clone_id if set and not provided (but not for clones table itself)
$currentClone = g::get("db.current_clone");
if ($currentClone && !isset($data['clone_id']) && $table !== 'clones') {
$data['clone_id'] = $currentClone;
}
// Convert arrays/objects to JSON
foreach ($data as $key => $value) {
if (is_array($value) || is_object($value)) {
$data[$key] = json_encode($value);
}
}
try {
$columns = array_keys($data);
$quotedColumns = array_map(function ($col) {
return "`$col`";
}, $columns);
$placeholders = array_fill(0, count($columns), '?');
$sql = "INSERT INTO $table (" . implode(', ', $quotedColumns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $pdo->prepare($sql);
$stmt->execute(array_values($data));
g::run("log.info", "Inserted into $table: {$data['hash']}");
return $data['hash'];
} catch (PDOException $e) {
g::run("log.error", "Insert failed: " . $e->getMessage());
return false;
}
},
/**
* Update records
*
* @param string $table Table name
* @param array $data Data to update
* @param array $where Where conditions
* @param string $connection Connection name
* @return bool Success status
*
* @example g::run("db.update", "items",
* array("state" => "published"),
* array("hash" => $hash)
* );
*/
"update" => function ($table, $data, $where, $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
// Add updated_at
$data['updated_at'] = g::run("core.now");
// Convert arrays/objects to JSON
foreach ($data as $key => $value) {
if (is_array($value) || is_object($value)) {
$data[$key] = json_encode($value);
}
}
try {
$setClauses = array();
$values = array();
foreach ($data as $key => $value) {
$setClauses[] = "$key = ?";
$values[] = $value;
}
$whereClauses = array();
foreach ($where as $key => $value) {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
$sql = "UPDATE $table SET " . implode(', ', $setClauses) . " WHERE " . implode(' AND ', $whereClauses);
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
g::run("log.info", "Updated $table: " . $stmt->rowCount() . " rows");
return true;
} catch (PDOException $e) {
g::run("log.error", "Update failed: " . $e->getMessage());
return false;
}
},
/**
* Delete records (soft delete by default)
*
* @param string $table Table name
* @param array $where Where conditions
* @param bool $hard Hard delete (true) or soft delete (false)
* @param string $connection Connection name
* @return bool Success status
*
* @example g::run("db.delete", "items", array("hash" => $hash));
* @example g::run("db.delete", "items", array("hash" => $hash), true); // Hard delete
*/
"delete" => function ($table, $where, $hard = false, $connection = 'default') {
if ($hard) {
// Hard delete
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
try {
$whereClauses = array();
$values = array();
foreach ($where as $key => $value) {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
$sql = "DELETE FROM $table WHERE " . implode(' AND ', $whereClauses);
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
g::run("log.warning", "Hard deleted from $table: " . $stmt->rowCount() . " rows");
return true;
} catch (PDOException $e) {
g::run("log.error", "Delete failed: " . $e->getMessage());
return false;
}
} else {
// Soft delete (set state to 'deleted')
return g::run("db.update", $table, array("state" => "deleted"), $where, $connection);
}
},
/**
* Select records
*
* @param string $table Table name
* @param array $where Where conditions
* @param string $connection Connection name
* @param array $options Additional options (limit, order, etc.)
* @return array|false Results or false
*
* @example $items = g::run("db.select", "items", array("type" => "post", "state" => "published"));
* @example $items = g::run("db.select", "items", array("type" => "post"), "main", array("limit" => 10, "order" => "created_at DESC"));
*/
"select" => function ($table, $where = array(), $connection = 'default', $options = array()) {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
try {
$whereClauses = array();
$values = array();
// Add clone_id if set
$currentClone = g::get("db.current_clone");
if ($currentClone && !isset($where['clone_id'])) {
$where['clone_id'] = $currentClone;
}
foreach ($where as $key => $value) {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
$sql = "SELECT * FROM $table";
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
// Add ORDER BY
if (isset($options['order'])) {
$sql .= " ORDER BY " . $options['order'];
}
// Add LIMIT
if (isset($options['limit'])) {
$sql .= " LIMIT " . (int)$options['limit'];
}
// Add OFFSET
if (isset($options['offset'])) {
$sql .= " OFFSET " . (int)$options['offset'];
}
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
$results = $stmt->fetchAll();
// Decode JSON columns
foreach ($results as &$row) {
foreach ($row as $key => &$value) {
if (in_array($key, array('labels', 'meta', 'media', 'data', 'settings'))) {
$decoded = json_decode($value, true);
if ($decoded !== null) {
$value = $decoded;
}
}
}
}
return $results;
} catch (PDOException $e) {
g::run("log.error", "Select failed: " . $e->getMessage());
return false;
}
},
/**
* Get a single record by hash
*
* @param string $table Table name
* @param string $hash Record hash
* @param string $connection Connection name
* @return array|false Record or false
*
* @example $item = g::run("db.get", "items", $hash);
*/
"get" => function ($table, $hash, $connection = 'default') {
$results = g::run("db.select", $table, array("hash" => $hash), $connection);
if ($results && count($results) > 0) {
return $results[0];
}
return false;
},
/**
* Set current clone context
*
* All subsequent queries will auto-filter by this clone_id.
*
* @param string $cloneHash Clone hash
* @return void
*
* @example g::run("db.setClone", $cloneHash);
*/
"setClone" => function ($cloneHash) {
g::set("db.current_clone", $cloneHash);
g::run("log.info", "Clone context set: $cloneHash");
},
/**
* Create the genes schema (5 tables)
*
* Creates: clones, persons, items, labels, events
*
* @param string $connection Connection name
* @return bool Success status
*
* @example g::run("db.createSchema", "main");
*/
"createSchema" => function ($connection = 'default') {
$config = g::get("db.config.$connection");
if (!$config) {
g::run("log.error", "No config found for connection: $connection");
return false;
}
$driver = isset($config['driver']) ? $config['driver'] : 'mysql';
// Get appropriate schema
$schema = g::run("db.getSchema", $driver);
if (!$schema) {
return false;
}
// Execute each table creation
foreach ($schema as $tableName => $sql) {
// Skip special keys
if ($tableName === '_indexes') {
continue;
}
$result = g::run("db.query", $sql, array(), $connection);
if ($result === false) {
g::run("log.error", "Failed to create table: $tableName");
return false;
}
g::run("log.info", "Created table: $tableName");
}
// Execute indexes if present (SQLite)
if (isset($schema['_indexes']) && is_array($schema['_indexes'])) {
foreach ($schema['_indexes'] as $indexSql) {
$result = g::run("db.query", $indexSql, array(), $connection);
if ($result === false) {
g::run("log.warning", "Failed to create index");
}
}
g::run("log.info", "Created indexes");
}
g::run("log.info", "Schema created successfully");
// Check if setup is needed
$mainConfig = g::get("config");
if (!isset($mainConfig['setup']) || !isset($mainConfig['setup']['completed']) || $mainConfig['setup']['completed'] !== true) {
g::run("log.info", "Running initial setup...");
g::run("db.autoSeed", $connection);
// Mark setup as completed
$mainConfig['setup'] = array(
'completed' => true,
'completed_at' => date('Y-m-d H:i:s'),
'version' => '1.0.0'
);
g::set("config", $mainConfig);
// Save to config.json
$configPath = DATA_FOLDER . "/config.json";
file_put_contents($configPath, json_encode($mainConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
g::run("log.info", "Setup completed and saved to config.json");
} else {
g::run("log.info", "Setup already completed, skipping autoSeed");
}
return true;
},
/**
* Auto-seed database with initial data
*
* Seeds:
* 1. Clone record (from config.clone) - only if not exists
* 2. Admin user (from config.admin) - only if not exists
* 3. Generic labels (person.state, item.state, etc.) - only if not exist
* 4. Custom labels (from config.seed.labels) - only if not exist
*
* Safe to run multiple times - checks for duplicates
*
* @param string $connection Connection name
* @return bool Success status
*/
"autoSeed" => function ($connection = 'default') {
g::run("log.info", "Auto-seeding database...");
$config = g::get("config");
// 1. Create clone record (only if not exists)
if (isset($config['clone'])) {
// Check if clone with this domain already exists
$existingClone = g::run("db.select", "clones", array("domain" => $config['clone']['domain']), $connection, array("limit" => 1));
if (empty($existingClone)) {
$cloneData = array(
'name' => $config['clone']['name'],
'domain' => $config['clone']['domain'],
'type' => isset($config['clone']['type']) ? $config['clone']['type'] : 'platform',
'state' => isset($config['clone']['state']) ? $config['clone']['state'] : 'active'
);
// Add settings if exists
if (isset($config['clone']['settings'])) {
$cloneData['settings'] = is_array($config['clone']['settings'])
? json_encode($config['clone']['settings'])
: $config['clone']['settings'];
}
$cloneHash = g::run("db.insert", "clones", $cloneData, $connection);
if ($cloneHash) {
g::run("log.info", "Created clone: " . $cloneData['name'] . " (hash: $cloneHash)");
// Set as current clone
g::set("db.current_clone", $cloneHash);
// Create setup event
$eventData = array(
'type' => 'clone.setup',
'ref1' => $cloneHash,
'data' => json_encode(array(
'action' => 'clone_created',
'domain' => $cloneData['domain'],
'timestamp' => date('Y-m-d H:i:s')
))
);
g::run("db.insert", "events", $eventData, $connection);
} else {
g::run("log.error", "Failed to create clone");
}
} else {
g::run("log.info", "Clone already exists, skipping");
// Set existing clone as current
g::set("db.current_clone", $existingClone[0]['hash']);
}
} else {
g::run("log.warning", "No clone config found in config.json");
}
// 2. Create admin user (only if not exists)
if (isset($config['admin'])) {
$adminConfig = $config['admin'];
// Check if admin with this email already exists
$existingAdmin = g::run("db.select", "persons", array("email" => $adminConfig['email']), $connection, array("limit" => 1));
if (empty($existingAdmin)) {
// Extract only the fields that match the persons schema
$admin = array(
'email' => isset($adminConfig['email']) ? $adminConfig['email'] : '',
'alias' => isset($adminConfig['alias']) ? $adminConfig['alias'] : 'admin',
'name' => isset($adminConfig['name']) ? $adminConfig['name'] : 'Admin',
'type' => isset($adminConfig['type']) ? $adminConfig['type'] : 'admin',
'state' => isset($adminConfig['state']) ? $adminConfig['state'] : 'active'
);
// Hash password
if (isset($adminConfig['password'])) {
$admin['password'] = g::run("auth.hash", $adminConfig['password']);
}
// Add meta
$admin['meta'] = json_encode(array('role' => 'admin'));
$adminHash = g::run("db.insert", "persons", $admin, $connection);
if ($adminHash) {
g::run("log.info", "Created admin: " . $admin['email'] . " (hash: $adminHash)");
// Create admin setup event
$eventData = array(
'type' => 'admin.setup',
'person_id' => $adminHash,
'data' => json_encode(array(
'action' => 'admin_created',
'email' => $admin['email'],
'timestamp' => date('Y-m-d H:i:s')
))
);
g::run("db.insert", "events", $eventData, $connection);
} else {
g::run("log.error", "Failed to create admin user");
}
} else {
g::run("log.info", "Admin user already exists, skipping");
}
} else {
g::run("log.warning", "No admin config found in config.json");
}
// 3. Create generic labels
$genericLabels = array(
// Person states - using name field for dotted notation
array("name" => "person.state.active", "key" => "active", "type" => "person.state"),
array("name" => "person.state.suspended", "key" => "suspended", "type" => "person.state"),
array("name" => "person.state.deleted", "key" => "deleted", "type" => "person.state"),
// Person types
array("name" => "person.type.user", "key" => "user", "type" => "person.type"),
array("name" => "person.type.persona", "key" => "persona", "type" => "person.type"),
// Item states
array("name" => "item.state.draft", "key" => "draft", "type" => "item.state"),
array("name" => "item.state.published", "key" => "published", "type" => "item.state"),
array("name" => "item.state.deleted", "key" => "deleted", "type" => "item.state"),
// Item types
array("name" => "item.type.post", "key" => "post", "type" => "item.type"),
array("name" => "item.type.page", "key" => "page", "type" => "item.type"),
array("name" => "item.type.comment", "key" => "comment", "type" => "item.type")
);
$labelCount = 0;
foreach ($genericLabels as $labelData) {
// Check if label already exists
$existingLabel = g::run("db.select", "labels", array("name" => $labelData['name']), $connection, array("limit" => 1));
if (empty($existingLabel)) {
$labelData['state'] = 'active';
$labelData['meta'] = json_encode(array("generic" => true));
$labelHash = g::run("db.insert", "labels", $labelData, $connection);
if ($labelHash) {
$labelCount++;
}
}
}
g::run("log.info", "Created $labelCount generic labels (skipped existing)");
// 4. Create custom labels from config
if (isset($config['seed']['labels']) && is_array($config['seed']['labels'])) {
$customCount = 0;
foreach ($config['seed']['labels'] as $seedLabel) {
// Map config format (name, label, value) to schema format (name, key, type)
$labelData = array(
'name' => isset($seedLabel['name']) ? $seedLabel['name'] : '',
'key' => isset($seedLabel['value']) ? $seedLabel['value'] : (isset($seedLabel['key']) ? $seedLabel['key'] : ''),
'type' => isset($seedLabel['type']) ? $seedLabel['type'] : 'custom',
'state' => 'active'
);
// Check if label already exists
$existingLabel = g::run("db.select", "labels", array("name" => $labelData['name']), $connection, array("limit" => 1));
if (empty($existingLabel)) {
// Store the display label in meta
$meta = array('custom' => true);
if (isset($seedLabel['label'])) {
$meta['label'] = $seedLabel['label'];
}
$labelData['meta'] = json_encode($meta);
$labelHash = g::run("db.insert", "labels", $labelData, $connection);
if ($labelHash) {
$customCount++;
}
}
}
g::run("log.info", "Created $customCount custom labels (skipped existing)");
}
// Create setup complete event
$cloneHash = g::get("db.current_clone");
if ($cloneHash) {
$eventData = array(
'type' => 'clone.setup_complete',
'ref1' => $cloneHash,
'data' => json_encode(array(
'action' => 'setup_completed',
'labels_created' => $labelCount,
'timestamp' => date('Y-m-d H:i:s')
))
);
g::run("db.insert", "events", $eventData, $connection);
}
g::run("log.info", "Auto-seed complete");
return true;
},
/**
* Get schema SQL for specific driver
*
* @param string $driver Database driver
* @return array|false Schema SQL statements
*/
"getSchema" => function ($driver) {
$schema = array();
if ($driver === 'mysql') {
$schema['clones'] = "CREATE TABLE IF NOT EXISTS clones (
hash VARCHAR(32) PRIMARY KEY,
type VARCHAR(50),
state VARCHAR(20) DEFAULT 'active',
name VARCHAR(255),
domain VARCHAR(255),
settings JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by VARCHAR(32),
updated_by VARCHAR(32),
INDEX(state),
INDEX(domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$schema['persons'] = "CREATE TABLE IF NOT EXISTS persons (
hash VARCHAR(32) PRIMARY KEY,
clone_id VARCHAR(32),
type VARCHAR(50),
state VARCHAR(20) DEFAULT 'active',
email VARCHAR(255) UNIQUE,
alias VARCHAR(100),
password VARCHAR(255),
name VARCHAR(255),
labels JSON,
meta JSON,
media JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by VARCHAR(32),
updated_by VARCHAR(32),
INDEX(clone_id, state),
INDEX(email),
INDEX(alias)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$schema['items'] = "CREATE TABLE IF NOT EXISTS items (
hash VARCHAR(32) PRIMARY KEY,
clone_id VARCHAR(32),
type VARCHAR(50),
state VARCHAR(20) DEFAULT 'active',
title VARCHAR(500),
link VARCHAR(1000),
safe_url VARCHAR(500),
blurb TEXT,
text LONGTEXT,
labels JSON,
meta JSON,
media JSON,
data JSON,
start_at TIMESTAMP NULL,
end_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by VARCHAR(32),
updated_by VARCHAR(32),
UNIQUE(clone_id, safe_url),
INDEX(clone_id, type, state),
INDEX(clone_id, safe_url),
INDEX(start_at, end_at),
INDEX(created_at),
FULLTEXT(title, blurb, text)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$schema['labels'] = "CREATE TABLE IF NOT EXISTS labels (
hash VARCHAR(32) PRIMARY KEY,
clone_id VARCHAR(32),
type VARCHAR(50),
state VARCHAR(20) DEFAULT 'active',
`key` VARCHAR(255),
name VARCHAR(255),
labels JSON,
meta JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by VARCHAR(32),
updated_by VARCHAR(32),
UNIQUE(clone_id, type, `key`),
INDEX(clone_id, type),
INDEX(clone_id, `key`),
INDEX(name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$schema['events'] = "CREATE TABLE IF NOT EXISTS events (
hash VARCHAR(32) PRIMARY KEY,
clone_id VARCHAR(32) NOT NULL,
person_id VARCHAR(32),
item_id VARCHAR(32),
type VARCHAR(50) NOT NULL,
state VARCHAR(20) DEFAULT 'active',
ref1 VARCHAR(255),
ref2 VARCHAR(255),
ref3 VARCHAR(255),
ref4 VARCHAR(255),
labels JSON,
data JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX(clone_id, type, created_at),
INDEX(person_id, created_at),
INDEX(item_id, created_at),
INDEX(ref1),
INDEX(ref2),
INDEX(created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
} elseif ($driver === 'sqlite') {
$schema['clones'] = "CREATE TABLE IF NOT EXISTS clones (
hash TEXT PRIMARY KEY,
type TEXT,
state TEXT DEFAULT 'active',
name TEXT,
domain TEXT,
settings TEXT,
created_at TEXT,
updated_at TEXT,
created_by TEXT,
updated_by TEXT
)";
$schema['persons'] = "CREATE TABLE IF NOT EXISTS persons (
hash TEXT PRIMARY KEY,
clone_id TEXT,
type TEXT,
state TEXT DEFAULT 'active',
email TEXT UNIQUE,
alias TEXT,
password TEXT,
name TEXT,
labels TEXT,
meta TEXT,
media TEXT,
created_at TEXT,
updated_at TEXT,
created_by TEXT,
updated_by TEXT
)";
$schema['items'] = "CREATE TABLE IF NOT EXISTS items (
hash TEXT PRIMARY KEY,
clone_id TEXT,
type TEXT,
state TEXT DEFAULT 'active',
title TEXT,
link TEXT,
safe_url TEXT,
blurb TEXT,
text TEXT,
labels TEXT,
meta TEXT,
media TEXT,
data TEXT,
start_at TEXT,
end_at TEXT,
created_at TEXT,
updated_at TEXT,
created_by TEXT,
updated_by TEXT,
UNIQUE(clone_id, safe_url)
)";
$schema['labels'] = "CREATE TABLE IF NOT EXISTS labels (
hash TEXT PRIMARY KEY,
clone_id TEXT,
type TEXT,
state TEXT DEFAULT 'active',
key TEXT,
name TEXT,
labels TEXT,
meta TEXT,
created_at TEXT,
updated_at TEXT,
created_by TEXT,
updated_by TEXT,
UNIQUE(clone_id, type, key)
)";
$schema['events'] = "CREATE TABLE IF NOT EXISTS events (
hash TEXT PRIMARY KEY,
clone_id TEXT NOT NULL,
person_id TEXT,
item_id TEXT,
type TEXT NOT NULL,
state TEXT DEFAULT 'active',
ref1 TEXT,
ref2 TEXT,
ref3 TEXT,
ref4 TEXT,
labels TEXT,
data TEXT,
created_at TEXT,
updated_at TEXT
)";
// Create indexes for SQLite
$schema['_indexes'] = array(
"CREATE INDEX IF NOT EXISTS idx_clones_state ON clones(state)",
"CREATE INDEX IF NOT EXISTS idx_clones_domain ON clones(domain)",
"CREATE INDEX IF NOT EXISTS idx_persons_clone ON persons(clone_id, state)",
"CREATE INDEX IF NOT EXISTS idx_persons_email ON persons(email)",
"CREATE INDEX IF NOT EXISTS idx_persons_alias ON persons(alias)",
"CREATE INDEX IF NOT EXISTS idx_items_clone_type ON items(clone_id, type, state)",
"CREATE INDEX IF NOT EXISTS idx_items_safe_url ON items(clone_id, safe_url)",
"CREATE INDEX IF NOT EXISTS idx_items_created ON items(created_at)",
"CREATE INDEX IF NOT EXISTS idx_labels_clone_type ON labels(clone_id, type)",
"CREATE INDEX IF NOT EXISTS idx_labels_key ON labels(clone_id, key)",
"CREATE INDEX IF NOT EXISTS idx_events_clone_type ON events(clone_id, type, created_at)",
"CREATE INDEX IF NOT EXISTS idx_events_person ON events(person_id, created_at)",
"CREATE INDEX IF NOT EXISTS idx_events_item ON events(item_id, created_at)",
"CREATE INDEX IF NOT EXISTS idx_events_ref1 ON events(ref1)",
"CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at)"
);
}
// Add PostgreSQL schemas here in future
return $schema;
},
));
// ============================================================================
// PHASE 7: DATABASE CRUD OPERATIONS
// ============================================================================
g::def("db.crud", array(
/**
* List records with pagination, filtering, and search
*
* Generic list function that works for all tables.
* Supports pagination, filtering, search, and sorting.
*
* @param string $table Table name
* @param array $filters WHERE conditions
* @param array $options Options (page, limit, search, order)
* @param string $connection Connection name
* @return array Result with data, total, page info
*
* @example $result = g::run("db.crud.list", "items",
* array("type" => "post", "state" => "published"),
* array("page" => 1, "limit" => 10, "order" => "created_at DESC")
* );
* @example $result = g::run("db.crud.list", "items",
* array("type" => "post"),
* array("search" => "genes", "searchFields" => array("title", "blurb"))
* );
*/
"list" => function ($table, $filters = array(), $options = array(), $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return array('success' => false, 'error' => 'No database connection');
}
// Default options
$page = isset($options['page']) ? max(1, (int)$options['page']) : 1;
$limit = isset($options['limit']) ? max(1, min(1000, (int)$options['limit'])) : 50;
$offset = ($page - 1) * $limit;
$order = isset($options['order']) ? $options['order'] : 'created_at DESC';
$search = isset($options['search']) ? trim($options['search']) : '';
$searchFields = isset($options['searchFields']) ? $options['searchFields'] : array('title', 'name', 'blurb', 'text');
try {
// Build WHERE clause
$whereClauses = array();
$values = array();
// Add clone_id if set
$currentClone = g::get("db.current_clone");
if ($currentClone && !isset($filters['clone_id'])) {
$filters['clone_id'] = $currentClone;
}
// Add filters
foreach ($filters as $key => $value) {
if (is_array($value)) {
// IN clause
$placeholders = array_fill(0, count($value), '?');
$whereClauses[] = "$key IN (" . implode(',', $placeholders) . ")";
$values = array_merge($values, $value);
} else {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
}
// Add search
if ($search !== '') {
$searchClauses = array();
foreach ($searchFields as $field) {
$searchClauses[] = "$field LIKE ?";
$values[] = "%$search%";
}
if (!empty($searchClauses)) {
$whereClauses[] = "(" . implode(' OR ', $searchClauses) . ")";
}
}
// Build SQL
$whereSQL = !empty($whereClauses) ? " WHERE " . implode(' AND ', $whereClauses) : "";
// Get total count
$countSQL = "SELECT COUNT(*) as total FROM $table" . $whereSQL;
$stmt = $pdo->prepare($countSQL);
$stmt->execute($values);
$total = (int)$stmt->fetch()['total'];
// Get data
$dataSQL = "SELECT * FROM $table" . $whereSQL . " ORDER BY $order LIMIT $limit OFFSET $offset";
$stmt = $pdo->prepare($dataSQL);
$stmt->execute($values);
$data = $stmt->fetchAll();
// Decode JSON columns
foreach ($data as &$row) {
foreach ($row as $key => &$value) {
if (in_array($key, array('labels', 'meta', 'media', 'data', 'settings'))) {
$decoded = json_decode($value, true);
if ($decoded !== null) {
$value = $decoded;
}
}
}
}
return array(
'success' => true,
'data' => $data,
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit),
'showing' => count($data)
);
} catch (PDOException $e) {
g::run("log.error", "CRUD list failed: " . $e->getMessage());
return array('success' => false, 'error' => $e->getMessage());
}
},
/**
* Count records with filters
*
* @param string $table Table name
* @param array $filters WHERE conditions
* @param string $connection Connection name
* @return int|false Count or false
*
* @example $count = g::run("db.crud.count", "items", array("type" => "post"));
*/
"count" => function ($table, $filters = array(), $connection = 'default') {
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
try {
$whereClauses = array();
$values = array();
// Add clone_id if set
$currentClone = g::get("db.current_clone");
if ($currentClone && !isset($filters['clone_id'])) {
$filters['clone_id'] = $currentClone;
}
foreach ($filters as $key => $value) {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
$sql = "SELECT COUNT(*) as total FROM $table";
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
return (int)$stmt->fetch()['total'];
} catch (PDOException $e) {
g::run("log.error", "CRUD count failed: " . $e->getMessage());
return false;
}
},
/**
* Full-text search across tables
*
* @param string $table Table name
* @param string $query Search query
* @param array $fields Fields to search
* @param array $options Options (limit, filters)
* @param string $connection Connection name
* @return array|false Results or false
*
* @example $results = g::run("db.crud.search", "items", "genes framework",
* array("title", "blurb", "text"),
* array("limit" => 20, "filters" => array("type" => "post"))
* );
*/
"search" => function ($table, $query, $fields = array(), $options = array(), $connection = 'default') {
if (empty($query)) {
return array();
}
$pdo = g::run("db.connection", $connection);
if (!$pdo) {
return false;
}
$limit = isset($options['limit']) ? max(1, min(1000, (int)$options['limit'])) : 50;
$filters = isset($options['filters']) ? $options['filters'] : array();
try {
$whereClauses = array();
$values = array();
// Add clone_id if set
$currentClone = g::get("db.current_clone");
if ($currentClone && !isset($filters['clone_id'])) {
$filters['clone_id'] = $currentClone;
}
// Add filters
foreach ($filters as $key => $value) {
$whereClauses[] = "$key = ?";
$values[] = $value;
}
// Add search
$searchClauses = array();
foreach ($fields as $field) {
$searchClauses[] = "$field LIKE ?";
$values[] = "%$query%";
}
if (!empty($searchClauses)) {
$whereClauses[] = "(" . implode(' OR ', $searchClauses) . ")";
}
$sql = "SELECT * FROM $table";
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
$sql .= " LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
$results = $stmt->fetchAll();
// Decode JSON columns
foreach ($results as &$row) {
foreach ($row as $key => &$value) {
if (in_array($key, array('labels', 'meta', 'media', 'data', 'settings'))) {
$decoded = json_decode($value, true);
if ($decoded !== null) {
$value = $decoded;
}
}
}
}
return $results;
} catch (PDOException $e) {
g::run("log.error", "CRUD search failed: " . $e->getMessage());
return false;
}
},
/**
* Validate data against table schema
*
* @param string $table Table name
* @param array $data Data to validate
* @param bool $isUpdate Is this an update operation
* @return array Result with valid flag and errors
*
* @example $validation = g::run("db.crud.validate", "items", $data);
*/
"validate" => function ($table, $data, $isUpdate = false) {
$errors = array();
// Define required fields per table
$requiredFields = array(
'clones' => array('name'),
'persons' => array('email'),
'items' => array('title'),
'labels' => array('name'),
'events' => array('type')
);
// Check required fields (only on insert)
if (!$isUpdate && isset($requiredFields[$table])) {
foreach ($requiredFields[$table] as $field) {
if (!isset($data[$field]) || trim($data[$field]) === '') {
$errors[] = "Field '$field' is required";
}
}
}
// Email validation for persons
if ($table === 'persons' && isset($data['email'])) {
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = "Invalid email format";
}
}
// URL validation
if (isset($data['link']) && !empty($data['link'])) {
if (!filter_var($data['link'], FILTER_VALIDATE_URL)) {
$errors[] = "Invalid URL format for 'link'";
}
}
return array(
'valid' => empty($errors),
'errors' => $errors
);
},
/**
* Export table data to JSON or CSV
*
* @param string $table Table name
* @param array $filters WHERE conditions
* @param string $format Format (json or csv)
* @param string $connection Connection name
* @return string|false Exported data or false
*
* @example $json = g::run("db.crud.export", "items", array("type" => "post"), "json");
* @example $csv = g::run("db.crud.export", "labels", array(), "csv");
*/
"export" => function ($table, $filters = array(), $format = 'json', $connection = 'default') {
$data = g::run("db.select", $table, $filters, $connection, array('limit' => 10000));
if ($data === false) {
return false;
}
if ($format === 'json') {
return json_encode($data, JSON_PRETTY_PRINT);
} elseif ($format === 'csv') {
if (empty($data)) {
return '';
}
$output = fopen('php://temp', 'r+');
// Header row
fputcsv($output, array_keys($data[0]));
// Data rows
foreach ($data as $row) {
// Convert arrays to JSON in CSV
foreach ($row as $key => $value) {
if (is_array($value)) {
$row[$key] = json_encode($value);
}
}
fputcsv($output, $row);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
return false;
},
/**
* Import data from JSON or CSV
*
* @param string $table Table name
* @param string $data Data to import
* @param string $format Format (json or csv)
* @param string $connection Connection name
* @return array Result with success count and errors
*
* @example $result = g::run("db.crud.import", "items", $jsonData, "json");
*/
"import" => function ($table, $data, $format = 'json', $connection = 'default') {
$records = array();
$success = 0;
$errors = array();
try {
if ($format === 'json') {
$records = json_decode($data, true);
if ($records === null) {
return array('success' => false, 'error' => 'Invalid JSON format');
}
} elseif ($format === 'csv') {
$lines = explode("\n", $data);
$headers = str_getcsv(array_shift($lines));
foreach ($lines as $line) {
if (trim($line) === '') continue;
$values = str_getcsv($line);
$record = array();
foreach ($headers as $i => $header) {
$value = isset($values[$i]) ? $values[$i] : '';
// Try to decode JSON values
$decoded = json_decode($value, true);
$record[$header] = ($decoded !== null) ? $decoded : $value;
}
$records[] = $record;
}
}
// Import each record
foreach ($records as $record) {
// Validate
$validation = g::run("db.crud.validate", $table, $record);
if (!$validation['valid']) {
$errors[] = array('record' => $record, 'errors' => $validation['errors']);
continue;
}
// Insert
$hash = g::run("db.insert", $table, $record, $connection);
if ($hash) {
$success++;
} else {
$errors[] = array('record' => $record, 'error' => 'Insert failed');
}
}
return array(
'success' => true,
'imported' => $success,
'total' => count($records),
'errors' => $errors
);
} catch (Exception $e) {
g::run("log.error", "CRUD import failed: " . $e->getMessage());
return array('success' => false, 'error' => $e->getMessage());
}
},
));
// ============================================================================
// PHASE 7.5: API SYSTEM
// ============================================================================
g::def("api", array(
/**
* Main API request handler
*
* Handles REST API requests for CRUD operations.
* Routes requests to appropriate handlers.
*
* @param string $path API path
* @param string $method HTTP method
* @param array $data Request data
* @return array API response
*
* @example $response = g::run("api.handle", "/items", "GET", array("type" => "post"));
* @example $response = g::run("api.handle", "/items/abc123", "GET");
* @example $response = g::run("api.handle", "/items", "POST", $data);
*/
"handle" => function ($path = null, $method = null, $data = null) {
// Auto-detect from request if not provided
if ($path === null) {
$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
}
if ($method === null) {
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
}
if ($data === null) {
if ($method === 'GET') {
$data = $_GET;
} elseif ($method === 'POST' || $method === 'PUT') {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if ($data === null) {
$data = $_POST;
}
}
}
// Remove leading/trailing slashes
$path = trim($path, '/');
$parts = explode('/', $path);
// Get table name (first part)
$table = isset($parts[0]) ? $parts[0] : '';
// Valid tables
$validTables = array('clones', 'persons', 'items', 'labels', 'events');
if (!in_array($table, $validTables)) {
return array('success' => false, 'error' => 'Invalid table');
}
// Check if specific hash is provided
$hash = isset($parts[1]) ? $parts[1] : null;
// Route to appropriate handler
try {
switch ($method) {
case 'GET':
if ($hash) {
// Get single record
$record = g::run("db.get", $table, $hash);
if ($record) {
return array('success' => true, 'data' => $record);
} else {
return array('success' => false, 'error' => 'Record not found');
}
} else {
// List records
$filters = isset($data['filters']) ? $data['filters'] : array();
$options = array(
'page' => isset($data['page']) ? $data['page'] : 1,
'limit' => isset($data['limit']) ? $data['limit'] : 50,
'order' => isset($data['order']) ? $data['order'] : 'created_at DESC',
'search' => isset($data['search']) ? $data['search'] : '',
'searchFields' => isset($data['searchFields']) ? $data['searchFields'] : array('title', 'name', 'blurb')
);
return g::run("db.crud.list", $table, $filters, $options);
}
break;
case 'POST':
// Create new record
$validation = g::run("db.crud.validate", $table, $data);
if (!$validation['valid']) {
return array('success' => false, 'errors' => $validation['errors']);
}
$newHash = g::run("db.insert", $table, $data);
if ($newHash) {
$record = g::run("db.get", $table, $newHash);
return array('success' => true, 'data' => $record, 'hash' => $newHash);
} else {
return array('success' => false, 'error' => 'Insert failed');
}
break;
case 'PUT':
case 'PATCH':
// Update record
if (!$hash) {
return array('success' => false, 'error' => 'Hash required for update');
}
$validation = g::run("db.crud.validate", $table, $data, true);
if (!$validation['valid']) {
return array('success' => false, 'errors' => $validation['errors']);
}
$updated = g::run("db.update", $table, $data, array('hash' => $hash));
if ($updated) {
$record = g::run("db.get", $table, $hash);
return array('success' => true, 'data' => $record);
} else {
return array('success' => false, 'error' => 'Update failed');
}
break;
case 'DELETE':
// Delete record
if (!$hash) {
return array('success' => false, 'error' => 'Hash required for delete');
}
$hard = isset($data['hard']) && $data['hard'] === true;
$deleted = g::run("db.delete", $table, array('hash' => $hash), $hard);
if ($deleted) {
return array('success' => true, 'message' => 'Record deleted');
} else {
return array('success' => false, 'error' => 'Delete failed');
}
break;
default:
return array('success' => false, 'error' => 'Method not allowed');
}
} catch (Exception $e) {
g::run("log.error", "API error: " . $e->getMessage());
return array('success' => false, 'error' => 'Internal server error');
}
},
/**
* Send JSON response
*
* @param array $data Response data
* @param int $statusCode HTTP status code
* @return void
*
* @example g::run("api.respond", array("success" => true, "data" => $items));
*/
"respond" => function ($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
},
/**
* Check API authentication
*
* @return bool Authenticated status
*
* @example if (g::run("api.checkAuth")) { ... }
*/
"checkAuth" => function () {
// Check for auth header
$headers = getallheaders();
$authHeader = isset($headers['Authorization']) ? $headers['Authorization'] : '';
if (empty($authHeader)) {
return false;
}
// Parse Bearer token
if (strpos($authHeader, 'Bearer ') === 0) {
$token = substr($authHeader, 7);
// Verify token (implement your logic here)
// For now, return true if token exists
return !empty($token);
}
return false;
},
));
// ============================================================================
// PHASE 7.75: AUTHENTICATION SYSTEM
// ============================================================================
g::def("auth", array(
/**
* Initialize authentication system
*
* Starts secure session and loads auth config.
* Call this at the start of your app if using auth.
*
* @return bool Success status
*
* @example g::run("auth.init");
*/
"init" => function () {
// Check if session already started
if (session_status() === PHP_SESSION_ACTIVE) {
return true;
}
// Get config for session name
$config = g::run("config.get");
$sessionName = isset($config['settings']['session_name']) ? $config['settings']['session_name'] : 'genes_session';
// Set secure session parameters
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_samesite', 'Lax');
// Use secure cookies if HTTPS
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
ini_set('session.cookie_secure', 1);
}
// Set session name and start
session_name($sessionName);
if (session_start()) {
g::run("log.info", "Authentication session initialized");
// Regenerate session ID periodically for security
if (!isset($_SESSION['created_at'])) {
$_SESSION['created_at'] = time();
} elseif (time() - $_SESSION['created_at'] > 3600) {
session_regenerate_id(true);
$_SESSION['created_at'] = time();
}
return true;
}
g::run("log.error", "Failed to start authentication session");
return false;
},
/**
* Get authentication mode from config
*
* @return string 'config' or 'database'
*
* @example $mode = g::run("auth.mode");
*/
"mode" => function () {
$config = g::run("config.get");
if (isset($config['auth']['mode'])) {
return $config['auth']['mode'];
}
// Default to config mode
return 'config';
},
/**
* Hash password securely
*
* Uses PASSWORD_BCRYPT or PASSWORD_ARGON2ID if available.
*
* @param string $password Plain text password
* @return string Hashed password
*
* @example $hash = g::run("auth.hash", "mypassword123");
*/
"hash" => function ($password) {
// Use ARGON2ID if available (PHP 7.3+), otherwise BCRYPT
if (defined('PASSWORD_ARGON2ID')) {
return password_hash($password, PASSWORD_ARGON2ID);
}
return password_hash($password, PASSWORD_BCRYPT);
},
/**
* Verify password against hash
*
* @param string $password Plain text password
* @param string $hash Hashed password
* @return bool True if password matches
*
* @example if (g::run("auth.verify", $password, $hash)) { ... }
*/
"verify" => function ($password, $hash) {
return password_verify($password, $hash);
},
/**
* Login user (config or database mode)
*
* Handles both config-based and database-based authentication.
* Includes rate limiting and login attempt tracking.
*
* @param string $username Username or email
* @param string $password Plain text password
* @return array Result with success status and user data or error
*
* @example $result = g::run("auth.login", "admin", "password123");
*/
"login" => function ($username, $password) {
$config = g::run("config.get");
$mode = g::run("auth.mode");
// Check if auth is enabled
if (isset($config['auth']['enabled']) && !$config['auth']['enabled']) {
return array('success' => false, 'error' => 'Authentication is disabled');
}
// Rate limiting check
$sessionKey = 'login_attempts_' . md5($username);
if (!isset($_SESSION[$sessionKey])) {
$_SESSION[$sessionKey] = array('count' => 0, 'last_attempt' => 0);
}
$attempts = &$_SESSION[$sessionKey];
$maxAttempts = isset($config['auth']['max_login_attempts']) ? $config['auth']['max_login_attempts'] : 5;
$lockoutDuration = isset($config['auth']['lockout_duration']) ? $config['auth']['lockout_duration'] : 900;
// Check if locked out
if ($attempts['count'] >= $maxAttempts) {
$timeSinceLastAttempt = time() - $attempts['last_attempt'];
if ($timeSinceLastAttempt < $lockoutDuration) {
$remainingTime = $lockoutDuration - $timeSinceLastAttempt;
return array(
'success' => false,
'error' => "Too many failed attempts. Try again in " . ceil($remainingTime / 60) . " minutes."
);
} else {
// Reset after lockout period
$attempts['count'] = 0;
}
}
$attempts['last_attempt'] = time();
// Authenticate based on mode
$user = false;
if ($mode === 'config') {
// Config-based authentication
$admins = isset($config['admins']) ? $config['admins'] : array();
foreach ($admins as $admin) {
if ($admin['username'] === $username || (isset($admin['email']) && $admin['email'] === $username)) {
if (g::run("auth.verify", $password, $admin['password'])) {
$user = $admin;
break;
}
}
}
} elseif ($mode === 'database') {
// Database-based authentication
$pdo = g::run("db.connection");
if (!$pdo) {
return array('success' => false, 'error' => 'Database connection failed');
}
// Find user by username or email
$users = g::run("db.select", "persons", array("alias" => $username));
if (empty($users)) {
$users = g::run("db.select", "persons", array("email" => $username));
}
if (!empty($users)) {
$dbUser = $users[0];
if (isset($dbUser['password']) && g::run("auth.verify", $password, $dbUser['password'])) {
$user = $dbUser;
// Update last login time
$meta = isset($user['meta']) ? $user['meta'] : array();
$meta['last_login'] = g::run("core.now");
$meta['login_attempts'] = 0;
g::run(
"db.update",
"persons",
array("meta" => $meta),
array("hash" => $user['hash'])
);
}
}
} elseif ($mode === 'file') {
// File-based authentication (users.json)
$fileUser = g::run("auth.getUser", $username);
if ($fileUser && isset($fileUser['password'])) {
if (g::run("auth.verify", $password, $fileUser['password'])) {
$user = $fileUser;
}
}
}
// Fallback: Try all auth methods if mode is not set or user not found yet
if (!$user && $mode !== 'config' && $mode !== 'database' && $mode !== 'file') {
// Try config first
$admins = isset($config['admins']) ? $config['admins'] : array();
foreach ($admins as $admin) {
if ($admin['username'] === $username || (isset($admin['email']) && $admin['email'] === $username)) {
if (g::run("auth.verify", $password, $admin['password'])) {
$user = $admin;
break;
}
}
}
// Try file-based if config didn't work
if (!$user) {
$fileUser = g::run("auth.getUser", $username);
if ($fileUser && isset($fileUser['password'])) {
if (g::run("auth.verify", $password, $fileUser['password'])) {
$user = $fileUser;
}
}
}
// Try database last if enabled
if (!$user) {
$pdo = g::run("db.connection");
if ($pdo) {
$users = g::run("db.select", "persons", array("alias" => $username));
if (empty($users)) {
$users = g::run("db.select", "persons", array("email" => $username));
}
if (!empty($users)) {
$dbUser = $users[0];
if (isset($dbUser['password']) && g::run("auth.verify", $password, $dbUser['password'])) {
$user = $dbUser;
}
}
}
}
}
// Check if authentication succeeded
if ($user) {
// Reset attempts on success
$attempts['count'] = 0;
// Store user in session
$_SESSION['auth_user'] = $user;
$_SESSION['auth_time'] = time();
// Generate CSRF token
$_SESSION['csrf_token'] = g::run("auth.generateToken");
g::run("log.info", "User logged in: " . $username);
return array(
'success' => true,
'user' => $user
);
} else {
// Increment failed attempts
$attempts['count']++;
g::run("log.warning", "Failed login attempt: " . $username);
return array(
'success' => false,
'error' => 'Invalid username or password'
);
}
},
/**
* Logout current user
*
* Clears session data and destroys session.
*
* @return bool Success status
*
* @example g::run("auth.logout");
*/
"logout" => function () {
if (session_status() === PHP_SESSION_ACTIVE) {
// Clear auth data
unset($_SESSION['auth_user']);
unset($_SESSION['auth_time']);
unset($_SESSION['csrf_token']);
// Regenerate session ID
session_regenerate_id(true);
g::run("log.info", "User logged out");
return true;
}
return false;
},
/**
* Check if user is logged in
*
* Also checks session timeout.
*
* @return bool True if user is authenticated
*
* @example if (g::run("auth.check")) { ... }
*/
"check" => function () {
if (session_status() !== PHP_SESSION_ACTIVE) {
return false;
}
if (!isset($_SESSION['auth_user']) || !isset($_SESSION['auth_time'])) {
return false;
}
// Check session timeout
$config = g::run("config.get");
$timeout = isset($config['auth']['session_timeout']) ? $config['auth']['session_timeout'] : 3600;
if (time() - $_SESSION['auth_time'] > $timeout) {
g::run("auth.logout");
return false;
}
// Update last activity time
$_SESSION['auth_time'] = time();
return true;
},
/**
* Get current logged-in user
*
* @return array|false User data or false if not logged in
*
* @example $user = g::run("auth.user");
*/
"user" => function () {
if (!g::run("auth.check")) {
return false;
}
$user = isset($_SESSION['auth_user']) ? $_SESSION['auth_user'] : false;
if ($user && isset($user['meta']) && is_string($user['meta'])) {
// Decode meta field if it's a JSON string
$user['meta'] = json_decode($user['meta'], true);
}
return $user;
},
/**
* Check if user can access a route
*
* Uses route patterns from config with wildcard matching.
*
* @param string $route Route to check (e.g., '/admin/items/create')
* @return bool True if user has access
*
* @example if (g::run("auth.can", "/admin/items/create")) { ... }
*/
"can" => function ($route) {
$user = g::run("auth.user");
if (!$user) {
return false;
}
// Get user's role
$role = isset($user['role']) ? $user['role'] : 'viewer';
// Get config
$config = g::run("config.get");
// Get routes for this role
$allowedRoutes = isset($config['routes'][$role]) ? $config['routes'][$role] : array();
// Check for wildcard (super admin)
if (in_array('*', $allowedRoutes)) {
return true;
}
// Check exact match
if (in_array($route, $allowedRoutes)) {
return true;
}
// Check pattern matches
foreach ($allowedRoutes as $pattern) {
// Convert route pattern to regex
// /admin/* becomes /^\/admin\/.*$/
// /admin/items/* becomes /^\/admin\/items\/.*$/
$regex = '/^' . str_replace(
array('/', '*'),
array('\/', '.*'),
$pattern
) . '$/';
if (preg_match($regex, $route)) {
return true;
}
}
return false;
},
/**
* Require authentication (or redirect/exit)
*
* @param string $redirect Redirect URL if not logged in
* @return void
*
* @example g::run("auth.require", "/login");
*/
"require" => function ($redirect = '/admin/login') {
if (!g::run("auth.check")) {
header("Location: $redirect");
exit;
}
},
/**
* Require access to specific route (or redirect/exit)
*
* @param string $route Route pattern to check
* @param string $redirect Redirect URL if no access
* @return void
*
* @example g::run("auth.requireRoute", "/admin/items/delete");
*/
"requireRoute" => function ($route, $redirect = '/admin') {
if (!g::run("auth.check")) {
header("Location: /admin/login");
exit;
}
if (!g::run("auth.can", $route)) {
g::run("log.warning", "Access denied to route: $route");
header("Location: $redirect");
exit;
}
},
/**
* Generate session token (for CSRF protection)
*
* @return string Random token
*
* @example $token = g::run("auth.generateToken");
*/
"generateToken" => function () {
if (function_exists('random_bytes')) {
return bin2hex(random_bytes(32));
} else {
return bin2hex(openssl_random_pseudo_bytes(32));
}
},
/**
* Validate CSRF token
*
* @param string $token Token to validate
* @return bool True if token is valid
*
* @example if (g::run("auth.validateToken", $_POST['csrf_token'])) { ... }
*/
"validateToken" => function ($token) {
if (session_status() !== PHP_SESSION_ACTIVE) {
return false;
}
if (!isset($_SESSION['csrf_token'])) {
return false;
}
// Use constant-time comparison to prevent timing attacks
return hash_equals($_SESSION['csrf_token'], $token);
},
// ========================================================================
// FILE-BASED USER MANAGEMENT (for clones without databases)
// ========================================================================
/**
* Load users from JSON file
*
* File-based user storage for simple setups.
*
* @return array Users data
*
* @example $users = g::run("auth.loadUsers");
*/
"loadUsers" => function () {
$usersFile = DATA_FOLDER . 'users.json';
if (!file_exists($usersFile)) {
// Create default users file with empty array
$defaultUsers = array();
g::run("config.writeJson", $defaultUsers, $usersFile);
return $defaultUsers;
}
$users = g::run("config.readJson", $usersFile);
return is_array($users) ? $users : array();
},
/**
* Save users to JSON file
*
* @param array $users Users data
* @return bool Success status
*
* @example g::run("auth.saveUsers", $users);
*/
"saveUsers" => function ($users) {
$usersFile = DATA_FOLDER . 'users.json';
return g::run("config.writeJson", $users, $usersFile);
},
/**
* Get user by username from file
*
* @param string $username Username to find
* @return array|null User data or null
*
* @example $user = g::run("auth.getUser", "john");
*/
"getUser" => function ($username) {
$users = g::run("auth.loadUsers");
foreach ($users as $user) {
if (isset($user['username']) && $user['username'] === $username) {
return $user;
}
}
return null;
},
/**
* Create or update a user in file
*
* Creates new user or updates existing one.
* Automatically hashes password if provided.
*
* @param array $userData User data (username, password, role, etc.)
* @return bool Success status
*
* @example g::run("auth.saveUser", [
* "username" => "john",
* "password" => "secret123",
* "role" => "user",
* "email" => "john@example.com"
* ]);
*/
"saveUser" => function ($userData) {
if (!isset($userData['username']) || empty($userData['username'])) {
g::run("log.error", "Cannot save user: username required");
return false;
}
$users = g::run("auth.loadUsers");
$username = $userData['username'];
$found = false;
// Hash password if provided and not already hashed
if (isset($userData['password']) && !empty($userData['password'])) {
// Check if already hashed (bcrypt/argon2)
if (substr($userData['password'], 0, 1) !== '$') {
$userData['password'] = g::run("auth.hash", $userData['password']);
}
}
// Set default role if not provided
if (!isset($userData['role'])) {
$userData['role'] = 'user';
}
// Add timestamps
$now = date('Y-m-d H:i:s');
if (!isset($userData['created_at'])) {
$userData['created_at'] = $now;
}
$userData['updated_at'] = $now;
// Update existing user or add new
for ($i = 0; $i < count($users); $i++) {
if ($users[$i]['username'] === $username) {
// Keep created_at from original
$userData['created_at'] = $users[$i]['created_at'];
// Keep password if not provided
if (!isset($userData['password']) || empty($userData['password'])) {
$userData['password'] = $users[$i]['password'];
}
$users[$i] = $userData;
$found = true;
break;
}
}
if (!$found) {
$users[] = $userData;
}
$result = g::run("auth.saveUsers", $users);
if ($result) {
g::run("log.info", "User saved to file: $username");
}
return $result;
},
/**
* Delete a user from file
*
* @param string $username Username to delete
* @return bool Success status
*
* @example g::run("auth.deleteUser", "john");
*/
"deleteUser" => function ($username) {
$users = g::run("auth.loadUsers");
$newUsers = array();
$found = false;
foreach ($users as $user) {
if ($user['username'] !== $username) {
$newUsers[] = $user;
} else {
$found = true;
}
}
if (!$found) {
g::run("log.warning", "User not found for deletion: $username");
return false;
}
$result = g::run("auth.saveUsers", $newUsers);
if ($result) {
g::run("log.info", "User deleted from file: $username");
}
return $result;
},
));
// ============================================================================
// PHASE 8: MOD SYSTEM
// ============================================================================
g::def("mod", array(
/**
* Get mod folder paths
*
* Returns array of paths to scan for mods:
* 1. Central mods (if genes.php is shared)
* 2. Local mods (in current clone folder)
*
* @return array Array of paths
*
* @example $paths = g::run("mod.getPaths");
*/
"getPaths" => function () {
$paths = array();
// Get config for custom paths
$config = g::run("config.get");
$modConfig = isset($config['mods']) ? $config['mods'] : array();
// 1. Check for central mods path from config
if (isset($modConfig['paths']['central']) && $modConfig['paths']['central']) {
$centralPath = $modConfig['paths']['central'];
if (!empty($centralPath) && is_dir($centralPath)) {
$paths['central'] = rtrim($centralPath, '/\\');
}
}
// 2. Auto-detect central mods (parent of genes.php location)
if (!isset($paths['central'])) {
$genesDir = dirname(__FILE__);
$centralModsDir = $genesDir . DIRECTORY_SEPARATOR . 'mods';
if (is_dir($centralModsDir)) {
$paths['central'] = $centralModsDir;
}
}
// 3. Check for local mods path from config
if (isset($modConfig['paths']['local']) && $modConfig['paths']['local']) {
$localPath = $modConfig['paths']['local'];
if (!empty($localPath) && is_dir($localPath)) {
$paths['local'] = rtrim($localPath, '/\\');
}
}
// 4. Auto-detect local mods (current working directory)
if (!isset($paths['local'])) {
$localModsDir = getcwd() . DIRECTORY_SEPARATOR . 'mods';
if (is_dir($localModsDir) && $localModsDir !== (isset($paths['central']) ? $paths['central'] : '')) {
$paths['local'] = $localModsDir;
}
}
return $paths;
},
/**
* Scan for available mods
*
* Scans central and local mod folders for PHP files.
* Returns array of mod names without .php extension.
*
* @return array Array of available mod names
*
* @example $mods = g::run("mod.scan");
* // Returns: array("analytics", "payment", "custom-blog")
*/
"scan" => function () {
$paths = g::run("mod.getPaths");
$mods = array();
// Scan each path
foreach ($paths as $pathType => $path) {
if (!is_dir($path)) {
continue;
}
$files = scandir($path);
foreach ($files as $file) {
if ($file === '.' || $file === '..' || $file === 'index.php') {
continue;
}
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$modName = pathinfo($file, PATHINFO_FILENAME);
// Store with path info
if (!isset($mods[$modName])) {
$mods[$modName] = array();
}
$mods[$modName][$pathType] = $path . DIRECTORY_SEPARATOR . $file;
}
}
}
g::set("mods.available", $mods);
g::run("log.info", "Scanned mods: " . count($mods) . " found");
return $mods;
},
/**
* Load enabled mods
*
* Loads mods based on config.json settings.
* Priority: Central first, then local (local can override).
*
* @return array Array of loaded mod names
*
* @example g::run("mod.load");
*/
"load" => function () {
$config = g::run("config.get");
$modConfig = isset($config['mods']) ? $config['mods'] : array();
// Get enabled mods from config
$enabled = isset($modConfig['enabled']) ? $modConfig['enabled'] : array();
$disabled = isset($modConfig['disabled']) ? $modConfig['disabled'] : array();
// If no enabled list, load all available mods except disabled
$availableMods = g::run("mod.scan");
if (empty($enabled)) {
// Load all except disabled
$enabled = array_keys($availableMods);
$enabled = array_diff($enabled, $disabled);
}
$loaded = array();
foreach ($enabled as $modName) {
if (in_array($modName, $disabled)) {
continue;
}
if (!isset($availableMods[$modName])) {
g::run("log.warning", "Mod not found: $modName");
continue;
}
$modPaths = $availableMods[$modName];
// Load central first (if exists)
if (isset($modPaths['central'])) {
try {
require_once $modPaths['central'];
$loaded[] = "$modName (central)";
g::run("log.info", "Loaded mod: $modName (central)");
} catch (Exception $e) {
g::run("log.error", "Failed to load mod $modName (central): " . $e->getMessage());
}
}
// Load local (can override central)
if (isset($modPaths['local'])) {
try {
require_once $modPaths['local'];
$loaded[] = "$modName (local)";
g::run("log.info", "Loaded mod: $modName (local)");
} catch (Exception $e) {
g::run("log.error", "Failed to load mod $modName (local): " . $e->getMessage());
}
}
}
g::set("mods.loaded", $loaded);
return $loaded;
},
/**
* Register mod with hooks and routes
*
* Mods call this to register their capabilities.
*
* @param string $modName Mod identifier
* @param array $config Mod configuration
* @return bool Success status
*
* @example g::run("mod.register", "analytics", array(
* "hooks" => array(
* "after_page_load" => "mod.analytics.track"
* ),
* "routes" => array(
* "/analytics" => "mod.analytics.dashboard"
* )
* ));
*/
"register" => function ($modName, $config) {
$registry = g::get("mods.registry");
if (!$registry) {
$registry = array();
}
$registry[$modName] = $config;
g::set("mods.registry", $registry);
// Register hooks
if (isset($config['hooks']) && is_array($config['hooks'])) {
foreach ($config['hooks'] as $hookName => $functionName) {
$hooks = g::get("mods.hooks.$hookName");
if (!$hooks) {
$hooks = array();
}
$hooks[] = array(
"mod" => $modName,
"function" => $functionName
);
g::set("mods.hooks.$hookName", $hooks);
}
}
// Register routes
if (isset($config['routes']) && is_array($config['routes'])) {
$routes = g::get("mods.routes");
if (!$routes) {
$routes = array();
}
foreach ($config['routes'] as $path => $handler) {
$routes[$path] = array(
"mod" => $modName,
"handler" => $handler
);
}
g::set("mods.routes", $routes);
}
g::run("log.info", "Registered mod: $modName");
return true;
},
/**
* Execute hook
*
* Runs all functions registered for a specific hook.
*
* @param string $hookName Hook identifier
* @param mixed $data Data to pass to hook functions
* @return mixed Last hook return value
*
* @example g::run("mod.hook", "after_page_load", array("url" => "/home"));
*/
"hook" => function ($hookName, $data = null) {
$hooks = g::get("mods.hooks.$hookName");
if (!$hooks || !is_array($hooks)) {
return null;
}
$result = null;
foreach ($hooks as $hook) {
$modName = $hook['mod'];
$functionName = $hook['function'];
if (!g::has($functionName)) {
g::run("log.warning", "Hook function not found: $functionName (mod: $modName)");
continue;
}
try {
$result = g::run($functionName, $data);
g::run("log.debug", "Executed hook: $hookName -> $functionName");
} catch (Exception $e) {
g::run("log.error", "Hook execution failed: $functionName - " . $e->getMessage());
}
}
return $result;
},
/**
* Execute mod function
*
* Runs a specific mod function by name.
* Convenience wrapper around g::run() for mod functions.
*
* @param string $functionName Full function name (e.g., "mod.analytics.track")
* @param mixed $params Parameters to pass
* @return mixed Function return value
*
* @example g::run("mod.execute", "mod.analytics.track", array("event" => "page_view"));
*/
"execute" => function ($functionName, $params = null) {
if (!g::has($functionName)) {
g::run("log.warning", "Mod function not found: $functionName");
return false;
}
return g::run($functionName, $params);
},
/**
* Get mod info
*
* Returns information about loaded mods and their registrations.
*
* @param string $modName Optional specific mod name
* @return array Mod information
*
* @example $info = g::run("mod.info", "analytics");
*/
"info" => function ($modName = null) {
if ($modName) {
$registry = g::get("mods.registry");
return isset($registry[$modName]) ? $registry[$modName] : null;
}
return array(
"paths" => g::run("mod.getPaths"),
"available" => g::get("mods.available"),
"loaded" => g::get("mods.loaded"),
"registry" => g::get("mods.registry"),
"hooks" => g::get("mods.hooks"),
"routes" => g::get("mods.routes")
);
},
));
// ============================================================================
// PHASE 9: DATA PROCESSING
// ============================================================================
g::def("data", array(
/**
* Validate data against rules
*
* Validates input data against a set of rules.
* Returns array with 'valid' boolean and 'errors' array.
*
* @param array $data Data to validate
* @param array $rules Validation rules
* @return array Result with 'valid' and 'errors' keys
*
* @example g::run("data.validate", $_POST, array(
* "email" => "required|email",
* "age" => "required|integer|min:18",
* "username" => "required|alphanumeric|min:3|max:20"
* ));
*/
"validate" => function ($data, $rules) {
$errors = array();
$valid = true;
foreach ($rules as $field => $ruleString) {
$value = isset($data[$field]) ? $data[$field] : null;
$fieldRules = explode('|', $ruleString);
foreach ($fieldRules as $rule) {
$ruleParts = explode(':', $rule);
$ruleName = $ruleParts[0];
$ruleParam = isset($ruleParts[1]) ? $ruleParts[1] : null;
$error = null;
switch ($ruleName) {
case 'required':
if (empty($value) && $value !== '0' && $value !== 0) {
$error = "$field is required";
}
break;
case 'email':
if ($value && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$error = "$field must be a valid email";
}
break;
case 'url':
if ($value && !filter_var($value, FILTER_VALIDATE_URL)) {
$error = "$field must be a valid URL";
}
break;
case 'integer':
case 'int':
if ($value && !filter_var($value, FILTER_VALIDATE_INT)) {
$error = "$field must be an integer";
}
break;
case 'numeric':
if ($value && !is_numeric($value)) {
$error = "$field must be numeric";
}
break;
case 'alpha':
if ($value && !ctype_alpha($value)) {
$error = "$field must contain only letters";
}
break;
case 'alphanumeric':
if ($value && !ctype_alnum($value)) {
$error = "$field must be alphanumeric";
}
break;
case 'min':
if ($value) {
$len = is_string($value) ? strlen($value) : $value;
if ($len < $ruleParam) {
$error = "$field must be at least $ruleParam";
}
}
break;
case 'max':
if ($value) {
$len = is_string($value) ? strlen($value) : $value;
if ($len > $ruleParam) {
$error = "$field must not exceed $ruleParam";
}
}
break;
case 'in':
if ($value) {
$allowed = explode(',', $ruleParam);
if (!in_array($value, $allowed)) {
$error = "$field must be one of: $ruleParam";
}
}
break;
case 'hash':
if ($value && strlen($value) !== 32) {
$error = "$field must be a valid hash (32 characters)";
}
break;
}
if ($error) {
$errors[$field][] = $error;
$valid = false;
}
}
}
return array(
'valid' => $valid,
'errors' => $errors
);
},
/**
* Sanitize data for safe output
*
* Cleans data to prevent XSS attacks.
* Can sanitize strings, arrays, or objects recursively.
*
* @param mixed $data Data to sanitize
* @param string $mode Sanitization mode: 'html', 'attr', 'url', 'js'
* @return mixed Sanitized data
*
* @example $clean = g::run("data.sanitize", $_POST, "html");
* @example $safeUrl = g::run("data.sanitize", $userInput, "url");
*/
"sanitize" => function ($data, $mode = 'html') {
if (is_array($data)) {
$result = array();
foreach ($data as $key => $value) {
$result[$key] = g::run("data.sanitize", $value, $mode);
}
return $result;
}
if (is_object($data)) {
$result = new stdClass();
foreach ($data as $key => $value) {
$result->$key = g::run("data.sanitize", $value, $mode);
}
return $result;
}
if (!is_string($data)) {
return $data;
}
switch ($mode) {
case 'html':
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
case 'attr':
return htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
case 'url':
return urlencode($data);
case 'js':
return json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
case 'sql':
// Note: Use prepared statements instead!
return addslashes($data);
case 'plain':
return strip_tags($data);
default:
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}
},
/**
* Filter array elements
*
* Filters array based on callback function or simple key-value match.
*
* @param array $array Array to filter
* @param mixed $callback Callback function or array of key=>value pairs
* @return array Filtered array
*
* @example $active = g::run("data.filter", $users, function($u) {
* return $u['state'] === 'active';
* });
*
* @example $active = g::run("data.filter", $users, array("state" => "active"));
*/
"filter" => function ($array, $callback) {
if (!is_array($array)) {
return array();
}
// Simple key-value filter
if (is_array($callback)) {
$result = array();
foreach ($array as $item) {
$match = true;
foreach ($callback as $key => $value) {
if (!isset($item[$key]) || $item[$key] !== $value) {
$match = false;
break;
}
}
if ($match) {
$result[] = $item;
}
}
return $result;
}
// Callback function filter
if (is_callable($callback)) {
return array_filter($array, $callback);
}
return $array;
},
/**
* Map/transform array elements
*
* Applies callback to each element and returns transformed array.
* Can also extract specific keys from array of arrays.
*
* @param array $array Array to map
* @param mixed $callback Callback function or key name to extract
* @return array Mapped array
*
* @example $names = g::run("data.map", $users, function($u) {
* return $u['name'];
* });
*
* @example $names = g::run("data.map", $users, "name");
*/
"map" => function ($array, $callback) {
if (!is_array($array)) {
return array();
}
// Extract key from each item
if (is_string($callback)) {
$key = $callback;
return array_map(function ($item) use ($key) {
return isset($item[$key]) ? $item[$key] : null;
}, $array);
}
// Callback function
if (is_callable($callback)) {
return array_map($callback, $array);
}
return $array;
},
/**
* Parse various data formats
*
* Parses JSON, query strings, CSV, or other formats into PHP arrays.
*
* @param string $data Data to parse
* @param string $format Format: 'json', 'query', 'csv', 'xml'
* @param array $options Optional parsing options
* @return mixed Parsed data
*
* @example $data = g::run("data.parse", $jsonString, "json");
* @example $params = g::run("data.parse", "foo=bar&baz=qux", "query");
* @example $rows = g::run("data.parse", $csvString, "csv");
*/
"parse" => function ($data, $format = 'json', $options = array()) {
switch ($format) {
case 'json':
$result = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
g::run("log.warning", "JSON parse error: " . json_last_error_msg());
return null;
}
return $result;
case 'query':
case 'querystring':
parse_str($data, $result);
return $result;
case 'csv':
$delimiter = isset($options['delimiter']) ? $options['delimiter'] : ',';
$enclosure = isset($options['enclosure']) ? $options['enclosure'] : '"';
$escape = isset($options['escape']) ? $options['escape'] : '\\';
$lines = explode("\n", trim($data));
$result = array();
// First line as headers if specified
$hasHeaders = isset($options['headers']) ? $options['headers'] : true;
$headers = null;
foreach ($lines as $i => $line) {
if (empty(trim($line))) {
continue;
}
$row = str_getcsv($line, $delimiter, $enclosure, $escape);
if ($hasHeaders && $i === 0) {
$headers = $row;
} else {
if ($headers) {
$row = array_combine($headers, $row);
}
$result[] = $row;
}
}
return $result;
case 'xml':
if (function_exists('simplexml_load_string')) {
$xml = @simplexml_load_string($data);
if ($xml === false) {
g::run("log.warning", "XML parse error");
return null;
}
return json_decode(json_encode($xml), true);
}
g::run("log.warning", "XML extension not available");
return null;
default:
g::run("log.warning", "Unknown parse format: $format");
return null;
}
},
/**
* Format date/time
*
* Converts date strings or timestamps into formatted strings.
* Handles various input formats and outputs in specified format.
*
* @param mixed $date Date string, timestamp, or DateTime object
* @param string $format Output format (PHP date format)
* @param string $default Default value if date is invalid
* @return string Formatted date string
*
* @example g::run("data.formatDate", "2025-10-10", "M d, Y"); // "Oct 10, 2025"
* @example g::run("data.formatDate", time(), "Y-m-d H:i:s");
* @example g::run("data.formatDate", "invalid", "Y-m-d", "N/A"); // "N/A"
*/
"formatDate" => function ($date, $format = 'Y-m-d H:i:s', $default = '') {
if (empty($date)) {
return $default;
}
// If it's already a timestamp
if (is_numeric($date)) {
return date($format, (int)$date);
}
// If it's a DateTime object
if ($date instanceof DateTime) {
return $date->format($format);
}
// Try to parse string
$timestamp = strtotime($date);
if ($timestamp === false) {
g::run("log.debug", "Could not parse date: $date");
return $default;
}
return date($format, $timestamp);
},
/**
* Get relative time (e.g., "2 hours ago", "in 3 days")
*
* Converts dates to human-readable relative format.
*
* @param mixed $date Date string or timestamp
* @param bool $detailed Whether to show detailed time (default: false)
* @return string Relative time string
*
* @example g::run("data.relativeTime", "2025-10-09"); // "1 day ago"
* @example g::run("data.relativeTime", time() - 3600); // "1 hour ago"
*/
"relativeTime" => function ($date, $detailed = false) {
if (empty($date)) {
return '';
}
// Convert to timestamp
if (is_numeric($date)) {
$timestamp = (int)$date;
} else {
$timestamp = strtotime($date);
if ($timestamp === false) {
return '';
}
}
$diff = time() - $timestamp;
$isPast = $diff >= 0;
$diff = abs($diff);
// Time units
$units = array(
31536000 => array('year', 'years'),
2592000 => array('month', 'months'),
604800 => array('week', 'weeks'),
86400 => array('day', 'days'),
3600 => array('hour', 'hours'),
60 => array('minute', 'minutes'),
1 => array('second', 'seconds')
);
foreach ($units as $seconds => $names) {
$count = floor($diff / $seconds);
if ($count >= 1) {
$unit = ($count == 1) ? $names[0] : $names[1];
$timeStr = $count . ' ' . $unit;
if ($detailed && $count < 10) {
$remainder = $diff % $seconds;
$nextUnit = null;
$nextCount = 0;
foreach ($units as $nextSeconds => $nextNames) {
if ($nextSeconds < $seconds) {
$nextCount = floor($remainder / $nextSeconds);
if ($nextCount >= 1) {
$nextUnit = ($nextCount == 1) ? $nextNames[0] : $nextNames[1];
break;
}
}
}
if ($nextUnit) {
$timeStr .= ' ' . $nextCount . ' ' . $nextUnit;
}
}
return $isPast ? $timeStr . ' ago' : 'in ' . $timeStr;
}
}
return 'just now';
},
));
// ============================================================================
// PHASE 10: TEMPLATE ENGINE (PROGRESSIVE ENHANCEMENT)
// ============================================================================
g::def("tpl", array(
/**
* Load template file
*
* Loads HTML template from file system with path resolution.
* Supports multiple template directories and caching.
*
* @param string $name Template name (e.g., "home", "user/profile")
* @param array $options Load options (paths, cache, etc.)
* @return string|false Template content or false on failure
*
* @example $html = g::run("tpl.load", "home");
* @example $html = g::run("tpl.load", "user/profile", array("cache" => true));
*/
"load" => function ($name, $options = array()) {
$defaults = array(
"paths" => array(
DATA_FOLDER . 'templates',
UI_FOLDER . 'tmpls',
WORK_DIR . 'templates'
),
"extension" => ".html",
"cache" => false
);
$config = array_merge($defaults, $options);
// Check cache first
if ($config['cache']) {
$cached = g::get("tpl.cache.$name");
if ($cached !== null) {
g::run("log.debug", "Template loaded from cache: $name");
return $cached;
}
}
// Clean up template name
$name = trim($name, '/');
$filename = $name . $config['extension'];
// Try each path
foreach ($config['paths'] as $path) {
$fullPath = $path . '/' . $filename;
if (file_exists($fullPath)) {
$content = file_get_contents($fullPath);
if ($content === false) {
g::run("log.error", "Could not read template: $fullPath");
continue;
}
g::run("log.debug", "Template loaded: $fullPath");
// Cache if requested
if ($config['cache']) {
g::set("tpl.cache.$name", $content);
}
return $content;
}
}
g::run("log.warning", "Template not found: $name (tried " . count($config['paths']) . " paths)");
return false;
},
/**
* Parse template HTML
*
* Extracts data-g-* attributes from HTML templates.
* Template authors can add data-a, data-b, data-c markers
* and matching comments (, , etc.) for nested elements.
*
* @param string $html HTML template content
* @return array Parsed template structure
*
* @example
* ...
* ...
*
*
*/
"parse" => function ($html) {
$elements = array();
// Find all elements with data-g-* attributes
$pattern = '/<([a-z][a-z0-9]*)\s+([^>]*data-g-[^>]*)>/i';
if (preg_match_all($pattern, $html, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
foreach ($matches as $match) {
$fullTag = $match[0][0];
$tagName = $match[1][0];
$attributes = $match[2][0];
$offset = $match[0][1];
// Extract data-g-* attributes
$dataAttrs = array();
if (preg_match_all('/data-g-([a-z]+)="([^"]*)"/i', $attributes, $attrMatches, PREG_SET_ORDER)) {
foreach ($attrMatches as $attrMatch) {
$attrName = $attrMatch[1];
$attrValue = $attrMatch[2];
$dataAttrs[$attrName] = $attrValue;
}
}
// Look for single-letter marker at the END: data-a, data-b, data-c, etc.
// Must be at the end (just before >) for simplicity and consistency
$marker = null;
if (preg_match('/data-([a-fh-z])\s*>$/i', $fullTag, $markerMatch)) {
$marker = $markerMatch[1];
}
$elements[] = array(
'tag' => $tagName,
'offset' => $offset,
'length' => strlen($fullTag),
'full' => $fullTag,
'attrs' => $dataAttrs,
'marker' => $marker // Single letter: a, b, c, etc.
);
}
}
// Mark which elements are nested inside others
// We'll calculate closing positions to determine nesting
foreach ($elements as $i => $elem) {
$marker = $elem['marker'];
if ($marker) {
// Find closing position using marker
$closeComment = '';
$closePos = strpos($html, $closeComment, $elem['offset']);
if ($closePos !== false) {
$elements[$i]['closePos'] = $closePos;
}
}
}
// Mark nested elements (elements inside other elements)
foreach ($elements as $i => $elem) {
$isNested = false;
if (isset($elem['closePos'])) {
foreach ($elements as $j => $other) {
if ($i !== $j && isset($other['closePos'])) {
// Is elem inside other?
if ($elem['offset'] > $other['offset'] && $elem['closePos'] < $other['closePos']) {
$isNested = true;
break;
}
}
}
}
$elements[$i]['isNested'] = $isNested;
}
g::run("log.debug", "Parsed " . count($elements) . " template elements");
return array(
'html' => $html,
'elements' => $elements
);
},
/**
* Render template with data
*
* Processes data-g-* attributes and renders HTML with provided data.
*
* Supported attributes:
* - data-g-bind: Bind text content (sanitized)
* - data-g-html: Bind raw HTML content
* - data-g-if: Conditional rendering with expressions:
* * Truthy: data-g-if="user" (checks if value exists and is truthy)
* * Negation: data-g-if="!user" (checks if value is falsy)
* * Equality: data-g-if="status:active" (checks if status equals "active")
* * Boolean: data-g-if="active:true" or data-g-if="disabled:false"
* * Number: data-g-if="count:0" (type-aware comparison)
* - data-g-for: Loop rendering (item in items)
* - data-g-load: Load partial templates
* - data-g-with: Pass data context to partials
* - data-g-attr: Bind attributes (name:path,name2:path2)
* - data-g-helper: Execute custom helper functions
*
* Progressive enhancement: HTML is valid and shows defaults,
* but gets enhanced with actual data server-side.
*
* @param string $html HTML template
* @param array $data Data to bind to template
* @param array $options Render options
* @return string Rendered HTML
*
* @example $output = g::run("tpl.render", $html, array(
* "user" => array("name" => "John", "email" => "john@example.com"),
* "posts" => array(...)
* ));
*/
"render" => function ($html, $data = array(), $options = array()) {
$defaults = array(
'sanitize' => true,
'helpers' => true
);
$config = array_merge($defaults, $options);
// First pass: Simple {{variable}} replacement for backwards compatibility
$html = preg_replace_callback('/\{\{([^}]+)\}\}/', function ($matches) use ($data) {
$path = trim($matches[1]);
$value = g::run("tpl._getValue", $path, $data);
if ($value === null) {
return $matches[0]; // Keep original if not found
}
// Return raw value without escaping for backwards compatibility
return $value;
}, $html);
// Parse template
$parsed = g::run("tpl.parse", $html);
$html = $parsed['html'];
$elements = $parsed['elements'];
// Filter: Skip elements nested inside data-g-for loops
// They'll be processed during loop iteration's recursive render
// But DO process elements nested in data-g-if (conditionals don't iterate)
$processableElements = array();
foreach ($elements as $elem) {
$skipThis = false;
if (isset($elem['isNested']) && $elem['isNested'] && isset($elem['closePos'])) {
// Check if nested inside a FOR loop
foreach ($elements as $parent) {
if (isset($parent['attrs']['for']) && isset($parent['closePos'])) {
if ($elem['offset'] > $parent['offset'] && $elem['closePos'] < $parent['closePos']) {
$skipThis = true;
break;
}
}
}
}
if (!$skipThis) {
$processableElements[] = $elem;
}
}
// Sort by offset (DESCENDING) to process from end to beginning
usort($processableElements, function ($a, $b) {
return $b['offset'] - $a['offset'];
});
foreach ($processableElements as $element) {
$attrs = $element['attrs'];
$replacement = null;
// Process data-g-if (conditional rendering)
if (isset($attrs['if'])) {
$condition = $attrs['if'];
$result = g::run("tpl._evaluateCondition", $condition, $data);
if (!$result) {
// Remove this element completely
$html = g::run("tpl._removeElement", $html, $element);
continue;
}
}
// Process data-g-for (loop rendering)
if (isset($attrs['for'])) {
$html = g::run("tpl._processFor", $html, $element, $data);
continue;
}
// Process data-g-load (load partial template)
if (isset($attrs['load'])) {
$html = g::run("tpl._processLoad", $html, $element, $data, $config);
continue;
}
// Process data-g-bind (text content binding)
if (isset($attrs['bind'])) {
$path = $attrs['bind'];
$value = g::run("tpl._getValue", $path, $data);
if ($value !== null) {
if ($config['sanitize']) {
$value = g::run("data.sanitize", $value, "html");
}
$html = g::run("tpl._setContent", $html, $element, $value);
}
}
// Process data-g-html (raw HTML binding)
if (isset($attrs['html'])) {
$path = $attrs['html'];
$value = g::run("tpl._getValue", $path, $data);
if ($value !== null) {
$html = g::run("tpl._setContent", $html, $element, $value);
}
}
// Process data-g-attr (attribute binding)
if (isset($attrs['attr'])) {
$attrBindings = $attrs['attr'];
// Format: "href:user.url,title:user.name"
$html = g::run("tpl._setAttributes", $html, $element, $attrBindings, $data);
}
// Process data-g-helper (custom helper function)
if (isset($attrs['helper']) && $config['helpers']) {
$helperName = $attrs['helper'];
$html = g::run("tpl._executeHelper", $html, $element, $helperName, $data);
}
}
// Clean up: Remove all data-g-* attributes from rendered HTML
$html = preg_replace('/\s+data-g-[a-z]+="[^"]*"/i', '', $html);
// Clean up: Remove marker attributes (data-a, data-b, etc.)
$html = preg_replace('/\s+data-[a-z](?=[\s>])/i', '', $html);
// Clean up: Remove marker comments (, , etc.)
$html = preg_replace('//i', '', $html);
return $html;
},
/**
* Render view template
*
* Loads and renders the template for a specific view.
* Checks view config for custom template, falls back to ui/index.html.
*
* @param string $viewName View name from config (e.g., "Index", "Admin")
* @param array $data Data to bind to template
* @param array $options Render options
* @return string|false Rendered HTML or false on failure
*
* @example echo g::run("tpl.renderView", "Admin", array("user" => $user));
* @example echo g::run("tpl.renderView", "Index", $data, array("template" => "custom"));
*/
"renderView" => function ($viewName, $data = array(), $options = array()) {
$config = g::get("config");
// Get views - merge domain-specific overrides
$views = isset($config["views"]) ? $config["views"] : array();
$currentDomain = g::get("current_domain");
$domainConfig = isset($config["domains"][$currentDomain]) ? $config["domains"][$currentDomain] : array();
// Merge domain-specific view overrides
if (isset($domainConfig["views_override"])) {
$views = array_merge($views, $domainConfig["views_override"]);
}
// Get view config
$viewConfig = isset($views[$viewName]) ? $views[$viewName] : null;
// ============================================================
// VALIDATION: Check for unhandled route segments or query params
// ============================================================
$request = g::get("request");
// Check if view allows additional segments (like /blog/post-slug)
$allowSegments = isset($options['allowSegments']) ? $options['allowSegments'] : false;
// Check if view allows query parameters (like /docs;section;topic)
$allowParams = isset($options['allowParams']) ? $options['allowParams'] : false;
// Check for unexpected route segments (path segments after the matched route)
if (!$allowSegments && $request && isset($request['route_segments']) && !empty($request['route_segments'])) {
// Unexpected segments found - this is a 404
http_response_code(404);
if (g::has("clone.NotFound")) {
$lang = isset($data['lang']) ? $data['lang'] : g::run("route.detectLanguage");
$bits = isset($data['bits']) ? $data['bits'] : array();
return g::run("clone.NotFound", $bits, $lang, $request['path']);
}
echo "404 - Page Not Found";
exit;
}
// Note: Standard URL query parameters (from ? in URL) are always allowed
// Only semicolon-delimited params need special handling if needed in the future
// ============================================================
// Determine template name
$templateName = null;
// 1. Check options override
if (isset($options['template'])) {
$templateName = $options['template'];
}
// 2. Check view config for custom template
else if ($viewConfig && isset($viewConfig['template'])) {
$templateName = $viewConfig['template'];
}
// 3. Fall back to index
else {
$templateName = 'index';
}
// Get current domain theme
$currentDomain = g::get("current_domain");
$domainConfig = g::get("domain_config");
$theme = isset($domainConfig['theme']) ? $domainConfig['theme'] : 'expo';
$circle = isset($domainConfig['circle']) ? $domainConfig['circle'] : null;
// Load template - try circles folder first, then UI folder
$templatePaths = array();
// If circle is defined, prioritize circles/{circle}/templates/
if ($circle) {
$templatePaths[] = './circles/' . $circle . '/templates';
}
// Then try UI folder with theme
$templatePaths[] = UI_FOLDER . $theme; // ui/rockers/, ui/expo/, etc.
$templatePaths[] = UI_FOLDER . 'expo'; // Fallback to expo templates
$templatePaths[] = UI_FOLDER; // Fallback to root ui/
$templatePaths[] = DATA_FOLDER . 'templates';
$templatePaths[] = UI_FOLDER . 'tmpls';
$html = g::run("tpl.load", $templateName, array(
"paths" => $templatePaths
));
if ($html === false) {
g::run("log.error", "Template not found for view: $viewName (template: $templateName, theme: $theme, circle: $circle)");
return false;
}
// Inject base href if not already present - always use current request URL
$baseUrl = g::run("core.getBaseUrl");
if (stripos($html, '';
$html = preg_replace('/(]*>)/i', '$1' . "\n " . $baseTag, $html, 1);
}
// Add meta information to data for template to use
if (!isset($data['meta'])) {
$data['meta'] = array();
}
// Add base URL to meta
$data['meta']['base_url'] = $baseUrl;
// Add canonical URL to meta (auto-generate from current URL)
if (!isset($data['meta']['canonical_url'])) {
$request = g::get("request");
if ($request) {
$data['meta']['canonical_url'] = $baseUrl . ltrim($request['path'], '/');
}
}
// Template can now use {{meta.base_url}}, {{meta.canonical_url}}, etc.
// Render with data
$rendered = g::run("tpl.render", $html, $data, $options);
// Check if this is an AJAX request from client-side router
// If so, return only the content columns (feed + details) for SPA navigation
$isAjax = g::get("response.is_ajax");
if ($isAjax) {
// Parse the rendered HTML to extract just the middle and right columns
$doc = new DOMDocument();
@$doc->loadHTML('' . $rendered);
$result = '';
// Extract middle column (feed)
$middleColumn = $doc->getElementById('feed');
if (!$middleColumn) {
// Try alternative selector
$xpath = new DOMXPath($doc);
$nodes = $xpath->query("//*[contains(@class, 'middle-column')]");
if ($nodes->length > 0) {
$middleColumn = $nodes->item(0);
}
}
// Extract right column (details)
$rightColumn = $doc->getElementById('details');
if (!$rightColumn) {
$xpath = new DOMXPath($doc);
$nodes = $xpath->query("//*[contains(@class, 'right-column')]");
if ($nodes->length > 0) {
$rightColumn = $nodes->item(0);
}
}
// Build partial HTML response
if ($middleColumn) {
$result .= $doc->saveHTML($middleColumn);
}
if ($rightColumn) {
$result .= $doc->saveHTML($rightColumn);
}
// If we successfully extracted columns, return them
// Otherwise fall back to full HTML
if (!empty($result)) {
return $result;
}
}
return $rendered;
},
/**
* Compile template
*
* Pre-processes template for faster repeated rendering.
* Extracts structure and creates optimized render function.
*
* @param string $html Template HTML
* @return array Compiled template data
*
* @example $compiled = g::run("tpl.compile", $html);
* @example $output = g::run("tpl.render", $compiled['html'], $data);
*/
"compile" => function ($html) {
$parsed = g::run("tpl.parse", $html);
$compiled = array(
'html' => $html,
'elements' => $parsed['elements'],
'compiled_at' => time(),
'stats' => array(
'element_count' => count($parsed['elements']),
'has_conditionals' => false,
'has_loops' => false,
'has_bindings' => false
)
);
// Analyze template for optimization hints
foreach ($parsed['elements'] as $element) {
if (isset($element['attrs']['if'])) {
$compiled['stats']['has_conditionals'] = true;
}
if (isset($element['attrs']['for'])) {
$compiled['stats']['has_loops'] = true;
}
if (isset($element['attrs']['bind']) || isset($element['attrs']['html'])) {
$compiled['stats']['has_bindings'] = true;
}
}
g::run("log.debug", "Template compiled with " . $compiled['stats']['element_count'] . " elements");
return $compiled;
},
/**
* Register template helper
*
* Registers custom helper function for use in templates.
* Helpers can format data, generate HTML, etc.
*
* @param string $name Helper name
* @param callable $callback Helper function
* @return void
*
* @example g::run("tpl.helper", "formatDate", function($date) {
* return date('M d, Y', strtotime($date));
* });
*
* @example g::run("tpl.helper", "money", function($amount) {
* return '$' . number_format($amount, 2);
* });
*/
"helper" => function ($name, $callback) {
if (!is_callable($callback)) {
g::run("log.error", "Template helper must be callable: $name");
return;
}
$helpers = g::get("tpl.helpers");
if (!is_array($helpers)) {
$helpers = array();
}
$helpers[$name] = $callback;
g::set("tpl.helpers", $helpers);
g::run("log.debug", "Registered template helper: $name");
},
// ========================================================================
// INTERNAL HELPER FUNCTIONS
// ========================================================================
/**
* Evaluate conditional expression (internal)
*
* Supports:
* - Negation: !user, !logged_in
* - Equality: status:active, role:admin, count:0
* - Truthy: user, logged_in
*
* @param string $condition Condition expression
* @param array $data Template data
* @return bool True if condition passes
*/
"_evaluateCondition" => function ($condition, $data) {
// Check for negation operator
$negated = false;
if (strpos($condition, '!') === 0) {
$negated = true;
$condition = substr($condition, 1);
}
// Check for comparison operator (key:value)
if (strpos($condition, ':') !== false) {
list($path, $expected) = explode(':', $condition, 2);
$value = g::run("tpl._getValue", $path, $data);
// Type-aware comparison
if ($expected === 'true') $expected = true;
else if ($expected === 'false') $expected = false;
else if ($expected === 'null') $expected = null;
else if (is_numeric($expected)) $expected = $expected + 0; // Convert to int/float
$result = ($value == $expected);
return $negated ? !$result : $result;
}
// Simple truthy check
$value = g::run("tpl._getValue", $condition, $data);
$result = !empty($value);
return $negated ? !$result : $result;
},
/**
* Get value from data by path (internal)
*/
"_getValue" => function ($path, $data) {
$parts = explode('.', $path);
$current = $data;
foreach ($parts as $part) {
if (is_array($current) && isset($current[$part])) {
$current = $current[$part];
} else if (is_object($current) && isset($current->$part)) {
$current = $current->$part;
} else {
return null;
}
}
return $current;
},
/**
* Set content inside element (internal)
*/
"_setContent" => function ($html, $element, $content) {
// Find MATCHING closing tag using marker
$tagName = $element['tag'];
$marker = isset($element['marker']) ? $element['marker'] : null;
$startPos = $element['offset'] + $element['length'];
$endPos = g::run("tpl._findClosingTag", $html, $tagName, $startPos, $marker);
if ($endPos === false) {
// Self-closing or no closing tag
return $html;
}
// Replace content between tags
$before = substr($html, 0, $startPos);
$after = substr($html, $endPos);
return $before . $content . $after;
},
/**
* Remove element completely (internal)
*/
"_removeElement" => function ($html, $element) {
$tagName = $element['tag'];
$marker = isset($element['marker']) ? $element['marker'] : null;
$startPos = $element['offset'];
$closeTag = '' . $tagName . '>';
// Find MATCHING closing tag using marker
$searchStart = $startPos + $element['length'];
$endPos = g::run("tpl._findClosingTag", $html, $tagName, $searchStart, $marker);
if ($endPos === false) {
// Remove just the opening tag
$before = substr($html, 0, $startPos);
$after = substr($html, $startPos + $element['length']);
return $before . $after;
}
// Remove entire element (including marker comment)
$before = substr($html, 0, $startPos);
$markerComment = $marker ? '' : '';
$after = substr($html, $endPos + strlen($closeTag) + strlen($markerComment));
return $before . $after;
},
/**
* Find matching closing tag using unique marker (internal)
*
* Looks for a closing tag with format:
* This is a simple, reliable way to handle nested tags.
*/
"_findClosingTag" => function ($html, $tagName, $startPos, $marker) {
$closeTag = '' . $tagName . '>';
$pos = strpos($html, $closeTag, $startPos);
if ($pos !== false) {
return $pos;
}
// Fallback: if no marker found, use simple close tag
// (for non-nested cases or already processed elements)
$simpleClose = '' . $tagName . '>';
return strpos($html, $simpleClose, $startPos);
},
/**
* Process data-g-for loop (internal)
*/
"_processFor" => function ($html, $element, $data) {
// Parse for syntax: "item in items" or "post in posts"
$forExpr = $element['attrs']['for'];
if (!preg_match('/(\w+)\s+in\s+([\w\.]+)/', $forExpr, $matches)) {
g::run("log.warning", "Invalid data-g-for syntax: $forExpr");
return $html;
}
$itemName = $matches[1];
$arrayPath = $matches[2];
// Get array data
$array = g::run("tpl._getValue", $arrayPath, $data);
if (!is_array($array) || empty($array)) {
// Remove the loop element
return g::run("tpl._removeElement", $html, $element);
}
// Extract element template
$tagName = $element['tag'];
$marker = isset($element['marker']) ? $element['marker'] : null;
$startPos = $element['offset'];
$closeTag = '' . $tagName . '>';
// Find MATCHING closing tag using marker
$searchStart = $startPos + $element['length'];
$endPos = g::run("tpl._findClosingTag", $html, $tagName, $searchStart, $marker);
if ($endPos === false) {
return $html;
}
$markerComment = $marker ? '' : '';
$templateStart = $startPos;
$templateEnd = $endPos + strlen($closeTag) + strlen($markerComment);
$template = substr($html, $templateStart, $templateEnd - $templateStart);
// Render for each item
$output = '';
foreach ($array as $index => $item) {
// Create context with item data
$itemData = array_merge($data, array(
$itemName => $item,
'_index' => $index,
'_first' => ($index === 0),
'_last' => ($index === count($array) - 1)
));
// Render this iteration
// Remove ONLY the outer data-g-for and marker (keep nested ones intact)
$itemHtml = $template;
// Remove the first occurrence of data-g-for (the outer one)
$itemHtml = preg_replace('/data-g-for="' . preg_quote($forExpr, '/') . '"\s*/', '', $itemHtml, 1);
// Remove the marker attribute and comment if present
if ($marker) {
// Remove data-X attribute at end: " data-a>" becomes ">"
$itemHtml = preg_replace('/\s*data-' . preg_quote($marker, '/') . '\s*>/', '>', $itemHtml, 1);
$itemHtml = str_replace($markerComment, '', $itemHtml);
}
$rendered = g::run("tpl.render", $itemHtml, $itemData, array('sanitize' => true, 'helpers' => false));
$output .= $rendered;
}
// Replace original element with rendered loop
$before = substr($html, 0, $templateStart);
$after = substr($html, $templateEnd);
return $before . $output . $after;
},
/**
* Process data-g-load (load partial template) (internal)
*
* Loads a partial template file and renders it with current data context.
* Supports passing data from parent scope or specific data path.
*
* @example
* @example
*/
"_processLoad" => function ($html, $element, $data, $config) {
$partialName = $element['attrs']['load'];
// Check if data-g-with attribute specifies a data path
$contextData = $data;
if (isset($element['attrs']['with'])) {
$dataPath = $element['attrs']['with'];
$specificData = g::run("tpl._getValue", $dataPath, $data);
if ($specificData !== null) {
// Merge specific data with parent data (specific data takes precedence)
if (is_array($specificData)) {
$contextData = array_merge($data, $specificData);
} else {
// If it's not an array, wrap it in 'item' key
$contextData = array_merge($data, array('item' => $specificData));
}
}
}
// Load the partial template
$partialHtml = g::run("tpl.load", $partialName, array(
"paths" => array(
UI_FOLDER . 'partials',
UI_FOLDER . 'tmpls',
DATA_FOLDER . 'templates',
WORK_DIR . 'templates'
),
"cache" => false // Don't cache partials during development
));
if ($partialHtml === false) {
g::run("log.warning", "Partial template not found: $partialName");
// Keep the original element or show error
$errorMsg = "";
return g::run("tpl._replaceElement", $html, $element, $errorMsg);
}
// Render the partial with context data
$rendered = g::run("tpl.render", $partialHtml, $contextData, array(
'sanitize' => isset($config['sanitize']) ? $config['sanitize'] : true,
'helpers' => isset($config['helpers']) ? $config['helpers'] : true
));
// Replace the element with rendered partial
return g::run("tpl._replaceElement", $html, $element, $rendered);
},
/**
* Replace element with new content (internal)
*
* Similar to _removeElement but replaces with new content instead of removing.
*/
"_replaceElement" => function ($html, $element, $newContent) {
$tagName = $element['tag'];
$marker = isset($element['marker']) ? $element['marker'] : null;
$startPos = $element['offset'];
$closeTag = '' . $tagName . '>';
// Find MATCHING closing tag using marker
$searchStart = $startPos + $element['length'];
$endPos = g::run("tpl._findClosingTag", $html, $tagName, $searchStart, $marker);
if ($endPos === false) {
// Replace just the opening tag
$before = substr($html, 0, $startPos);
$after = substr($html, $startPos + $element['length']);
return $before . $newContent . $after;
}
// Replace entire element (including marker comment)
$before = substr($html, 0, $startPos);
$markerComment = $marker ? '' : '';
$after = substr($html, $endPos + strlen($closeTag) + strlen($markerComment));
return $before . $newContent . $after;
},
/**
* Set attributes on element (internal)
*/
"_setAttributes" => function ($html, $element, $attrBindings, $data) {
// Parse attr bindings: "href:user.url,title:user.name"
$bindings = explode(',', $attrBindings);
$newTag = $element['full'];
foreach ($bindings as $binding) {
$parts = explode(':', trim($binding));
if (count($parts) !== 2) {
continue;
}
$attrName = trim($parts[0]);
$valuePath = trim($parts[1]);
$value = g::run("tpl._getValue", $valuePath, $data);
if ($value !== null) {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
// Check if attribute already exists
if (preg_match('/' . $attrName . '="[^"]*"/', $newTag)) {
// Replace existing
$newTag = preg_replace('/' . $attrName . '="[^"]*"/', $attrName . '="' . $value . '"', $newTag);
} else {
// Add new attribute
$newTag = str_replace('>', ' ' . $attrName . '="' . $value . '">', $newTag);
}
}
}
// Replace in HTML
$html = substr_replace($html, $newTag, $element['offset'], $element['length']);
return $html;
},
/**
* Execute helper function (internal)
*/
"_executeHelper" => function ($html, $element, $helperName, $data) {
$helpers = g::get("tpl.helpers");
if (!$helpers || !isset($helpers[$helperName])) {
g::run("log.warning", "Template helper not found: $helperName");
return $html;
}
$helper = $helpers[$helperName];
// Get element content
$tagName = $element['tag'];
$marker = isset($element['marker']) ? $element['marker'] : null;
$startPos = $element['offset'] + $element['length'];
// Find MATCHING closing tag using marker
$endPos = g::run("tpl._findClosingTag", $html, $tagName, $startPos, $marker);
if ($endPos === false) {
return $html;
}
$content = substr($html, $startPos, $endPos - $startPos);
// Execute helper
try {
$result = call_user_func($helper, $content, $data, $element);
if ($result !== null) {
$html = g::run("tpl._setContent", $html, $element, $result);
}
} catch (Exception $e) {
g::run("log.error", "Helper execution failed: $helperName - " . $e->getMessage());
}
return $html;
},
));
// ============================================================================
// AUTO-INITIALIZE
// ============================================================================
if (!defined('GENES_NO_AUTO_INIT')) {
g::run("core.init");
// Auto-log performance for each request
$logFile = LOG_FILE; // Capture constant in local scope
register_shutdown_function(function () use ($logFile) {
// Get performance metrics
$perfData = g::run("log.performance");
if ($perfData && isset($perfData['execution_time_ms'])) {
$duration = $perfData['execution_time_ms'];
$memory = $perfData['peak_memory_kb'];
// Get request info if available
$request = g::get("request");
$path = $request && isset($request['path']) ? $request['path'] : 'CLI';
$method = $request && isset($request['method']) ? $request['method'] : 'CLI';
// Create timestamp with milliseconds (direct implementation for shutdown context)
$now = microtime(true);
$seconds = floor($now);
$milliseconds = sprintf("%03d", round(($now - $seconds) * 1000));
$timestamp = date('Y-m-d H:i:s', $seconds) . '.' . $milliseconds;
$logMessage = "Request: $method $path | Time: {$duration}ms | Memory: {$memory}KB";
$logEntry = "[$timestamp] [PERF] $logMessage\n";
@file_put_contents($logFile, $logEntry, FILE_APPEND);
}
});
}
// END OF PHASE 0 + PHASE 1 + PHASE 2 + PHASE 3 + PHASE 4 + PHASE 5 + PHASE 6 + PHASE 7 + PHASE 8
// ============================================================================