functions.php
PHP
Path: functions.php
<?php
// Important: you MUST read MINI-STYLE.md before using Mini framework.
namespace mini;
use Exception;
use Throwable;
use Composer\Autoload\ClassLoader;
use mini\Http;
use mini\Mini;
use ReflectionClass;
/**
* Mini Framework - Global Helper Functions
*
* These functions are automatically loaded by Composer and available globally.
*/
/**
* Redirect to URL and exit
*
* @param string $url Target URL for redirect
* @param int $statusCode HTTP status code (301 for permanent, 302 for temporary)
*/
function redirect(string $url, int $statusCode = 302): void {
http_response_code($statusCode);
header('Location: ' . $url);
exit;
}
/**
* Escape HTML output
*/
function h($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
/**
* Get current URL
*/
function current_url() {
return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
. '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
/**
* Generate URL with proper relative path resolution and optional CDN support
*
* Resolves paths against baseUrl (or cdnUrl if $cdn = true), handling relative paths like
* '..', '.', and absolute paths. Strips scheme/host from input URLs to ensure all URLs
* resolve against the configured base.
*
* Examples:
* url('/api/users') → https://example.com/api/users
* url('css/style.css', cdn: true) → https://cdn.example.com/app/css/style.css
* url('../images/logo.png') → https://example.com/images/logo.png
* url('/assets/app.js', ['v' => '2']) → https://example.com/assets/app.js?v=2
*
* Returns UriInterface which is stringable, so works in templates:
* <a href="<?= url('/users') ?>">Users</a>
*
* Can chain PSR-7 methods for further manipulation:
* url('/posts')->withFragment('comments')->withQuery('page=2')
*
* @param string|\Psr\Http\Message\UriInterface $path Path to resolve (relative or absolute)
* @param array $query Query parameters to merge
* @param bool $cdn Use CDN base URL instead of regular base URL
* @return \Psr\Http\Message\UriInterface Resolved URL
* @throws Exception If base URL cannot be determined
*/
function url(string|\Psr\Http\Message\UriInterface $path = '', array $query = [], bool $cdn = false): \Psr\Http\Message\UriInterface {
$baseUrl = $cdn ? Mini::$mini->cdnUrl : Mini::$mini->baseUrl;
if ($baseUrl === null) {
throw new Exception('Base URL not configured. Set MINI_BASE_URL environment variable');
}
// Parse base URL
$base = new \mini\Http\Message\Uri($baseUrl);
// Extract path from input (strip scheme/host if present)
if ($path instanceof \Psr\Http\Message\UriInterface) {
$inputPath = $path->getPath();
$inputQuery = $path->getQuery();
$inputFragment = $path->getFragment();
} else {
// Parse string to extract path, query, fragment
$parsed = new \mini\Http\Message\Uri($path);
$inputPath = $parsed->getPath();
$inputQuery = $parsed->getQuery();
$inputFragment = $parsed->getFragment();
}
// Resolve path relative to base path
$basePath = $base->getPath();
if ($inputPath === '') {
// Empty path - use base path
$resolvedPath = $basePath;
} elseif ($inputPath[0] === '/') {
// Absolute path - prepend base path for subdirectory mounting
// e.g., url('/todos/') with baseUrl '/assistant' becomes '/assistant/todos/'
$resolvedPath = rtrim($basePath, '/') . $inputPath;
} else {
// Relative path - resolve against base path
// Append to base path directory
$baseDir = rtrim(dirname($basePath . '/dummy'), '/');
$resolvedPath = $baseDir . '/' . $inputPath;
}
// Normalize path (handle .. and .), preserving trailing slash
$hadTrailingSlash = str_ends_with($resolvedPath, '/');
$resolvedPath = (string) (new \mini\Util\Path($resolvedPath))->canonical();
if ($hadTrailingSlash && !str_ends_with($resolvedPath, '/')) {
$resolvedPath .= '/';
}
// Merge query parameters: input query + $query array
parse_str($inputQuery, $inputQueryParams);
$mergedQuery = http_build_query($inputQueryParams + $query);
// Build final URI
return $base
->withPath($resolvedPath)
->withQuery($mergedQuery)
->withFragment($inputFragment);
}
/**
* Flash message functions
*/
function flash_set($type, $message) {
if (!isset($_SESSION['flash'])) {
$_SESSION['flash'] = [];
}
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
}
/**
* Retrieve and clear all flash messages from the session
*
* Returns an array of flash messages and removes them from the session,
* ensuring each message is only displayed once.
*
* @return array<array{type: string, message: string}> Array of flash messages
*/
function flash_get(): array {
if (!isset($_SESSION['flash'])) {
return [];
}
$flash = $_SESSION['flash'];
unset($_SESSION['flash']);
return $flash;
}
// Note: session(), db() and cache() helpers are now in src/ feature directories
/**
* Bootstrap the mini framework for controller files
*
* Call this at the top of any directly-accessible PHP file in the document root.
* Sets up error handling, output buffering, and clean URL redirects.
*
* Transitions application from Bootstrap to Ready phase, enabling access to Scoped services.
*
* Safe to call multiple times (idempotent after first call).
*/
function bootstrap(): void
{
static $initialized = false;
if ($initialized) {
return; // Already bootstrapped
}
$initialized = true;
// Transition to Ready phase - enables request handling and access to Scoped services
Mini::$mini->phase->trigger(Phase::Ready);
// Clean up pre-existing output handlers
$previousLevel = -1;
while (ob_get_level() > 0 && ob_get_level() !== $previousLevel) {
$previousLevel = ob_get_level();
@ob_end_clean();
}
// Set up error handler (converts errors to exceptions)
set_error_handler(function($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
// Set up exception handler (fallback for bootstrap errors) - only if none exists
$existingHandler = set_exception_handler(null);
if ($existingHandler !== null) {
// Developer has their own exception handler - keep it
set_exception_handler($existingHandler);
} else {
// No handler exists - set Mini's fallback handler
// Note: When using dispatch(), Dispatcher handles exceptions during request lifecycle
set_exception_handler(function(\Throwable $exception) {
error_log("Uncaught exception: " . $exception->getMessage() . " in " . $exception->getFile() . " line " . $exception->getLine());
error_log("Stack trace: " . $exception->getTraceAsString());
if (headers_sent()) {
if (Mini::$mini->debug) {
echo $exception;
} else {
echo get_class($exception) . " thrown in " . $exception->getFile() . " line " . $exception->getLine();
}
die();
}
if (ob_get_level() > 0) {
ob_clean();
}
// Render basic error page
http_response_code(500);
echo "<h1>500 - Internal Server Error</h1>";
if (Mini::$mini->debug) {
echo "<pre>" . htmlspecialchars($exception->getMessage()) . "\n\n";
echo $exception->getTraceAsString() . "</pre>";
} else {
echo "<p>An unexpected error occurred.</p>";
}
});
}
// Start unlimited output buffering for exception recovery
// Buffer size 0 = unlimited, never auto-flush (prevents partial output on errors)
ob_start(null, 0);
// Parse application/json request bodies to $_POST (PHP doesn't do this natively)
if (str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'application/json')) {
$json = file_get_contents('php://input');
$data = json_decode($json, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
$_POST = $data;
}
}
// If routing is enabled and accessing /index.php directly, redirect to /
if (isset($GLOBALS['mini_routing_enabled']) && $GLOBALS['mini_routing_enabled']) {
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
$path = parse_url($requestUri, PHP_URL_PATH) ?? '';
$query = $_SERVER['QUERY_STRING'] ?? '';
// Redirect /index.php to /
if ($path === '/index.php' || str_ends_with($path, '/index.php')) {
$redirectTo = rtrim(dirname($path), '/') ?: '/';
if ($query) {
$redirectTo .= '?' . $query;
}
http_response_code(301);
header('Location: ' . $redirectTo);
exit;
}
}
}
/**
* Router entry point for applications with routing enabled
*
* Call this from DOC_ROOT/index.php to enable routing:
* - Sets up error handling and output buffering
* - Delegates URL routing to Router
* - Routes loaded from _routes/ directory
*
* Route handlers in _routes/ don't need to call bootstrap().
*/
// dispatch() function moved to src/Dispatcher/functions.php
/**
* Main entry point for file-based routing
*
* Bootstraps the application and routes the current request to
* the appropriate handler in the _routes/ directory.
*
* @deprecated Use dispatch() instead. Will be removed in future version.
*/
function router(): void
{
// Set global flag that routing is enabled
$GLOBALS['mini_routing_enabled'] = true;
// Bootstrap sets up error handlers, output buffering, etc.
bootstrap();
// Delegate routing to Router
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
$router = Mini::$mini->get(\mini\Router\Router::class);
$router->handleRequest($requestUri);
// Explicitly flush output buffer on successful completion
// (Exception handler discards buffer via ob_end_clean())
if (ob_get_level() > 0) {
ob_end_flush();
}
}
/**
* Create a CSRF token for a specific action
*
* Convenience wrapper around new CSRF().
*
* @param string $action Action name (e.g., 'delete-post', 'update-settings')
* @param string $fieldName HTML field name (default: '__nonce__')
* @return CSRF CSRF token object
*/
function csrf(string $action, string $fieldName = '__nonce__'): CSRF {
return new CSRF($action, $fieldName);
}