functions.php source

1 <?php
2 // Important: you MUST read MINI-STYLE.md before using Mini framework.
3 namespace mini;
4
5 use Exception;
6 use Throwable;
7 use Composer\Autoload\ClassLoader;
8 use mini\Http;
9 use mini\Mini;
10 use ReflectionClass;
11
12 /**
13 * Mini Framework - Global Helper Functions
14 *
15 * These functions are automatically loaded by Composer and available globally.
16 */
17
18 /**
19 * Redirect to URL and exit
20 *
21 * @param string $url Target URL for redirect
22 * @param int $statusCode HTTP status code (301 for permanent, 302 for temporary)
23 */
24 function redirect(string $url, int $statusCode = 302): void {
25 http_response_code($statusCode);
26 header('Location: ' . $url);
27 exit;
28 }
29
30 /**
31 * Escape HTML output
32 */
33 function h($str) {
34 return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
35 }
36
37 /**
38 * Get current URL
39 */
40 function current_url() {
41 return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')
42 . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
43 }
44
45 /**
46 * Generate URL with proper relative path resolution and optional CDN support
47 *
48 * Resolves paths against baseUrl (or cdnUrl if $cdn = true), handling relative paths like
49 * '..', '.', and absolute paths. Strips scheme/host from input URLs to ensure all URLs
50 * resolve against the configured base.
51 *
52 * Examples:
53 * url('/api/users') → https://example.com/api/users
54 * url('css/style.css', cdn: true) → https://cdn.example.com/app/css/style.css
55 * url('../images/logo.png') → https://example.com/images/logo.png
56 * url('/assets/app.js', ['v' => '2']) → https://example.com/assets/app.js?v=2
57 *
58 * Returns UriInterface which is stringable, so works in templates:
59 * <a href="<?= url('/users') ?>">Users</a>
60 *
61 * Can chain PSR-7 methods for further manipulation:
62 * url('/posts')->withFragment('comments')->withQuery('page=2')
63 *
64 * @param string|\Psr\Http\Message\UriInterface $path Path to resolve (relative or absolute)
65 * @param array $query Query parameters to merge
66 * @param bool $cdn Use CDN base URL instead of regular base URL
67 * @return \Psr\Http\Message\UriInterface Resolved URL
68 * @throws Exception If base URL cannot be determined
69 */
70 function url(string|\Psr\Http\Message\UriInterface $path = '', array $query = [], bool $cdn = false): \Psr\Http\Message\UriInterface {
71 $baseUrl = $cdn ? Mini::$mini->cdnUrl : Mini::$mini->baseUrl;
72
73 if ($baseUrl === null) {
74 throw new Exception('Base URL not configured. Set MINI_BASE_URL environment variable');
75 }
76
77 // Parse base URL
78 $base = new \mini\Http\Message\Uri($baseUrl);
79
80 // Extract path from input (strip scheme/host if present)
81 if ($path instanceof \Psr\Http\Message\UriInterface) {
82 $inputPath = $path->getPath();
83 $inputQuery = $path->getQuery();
84 $inputFragment = $path->getFragment();
85 } else {
86 // Parse string to extract path, query, fragment
87 $parsed = new \mini\Http\Message\Uri($path);
88 $inputPath = $parsed->getPath();
89 $inputQuery = $parsed->getQuery();
90 $inputFragment = $parsed->getFragment();
91 }
92
93 // Resolve path relative to base path
94 $basePath = $base->getPath();
95
96 if ($inputPath === '') {
97 // Empty path - use base path
98 $resolvedPath = $basePath;
99 } elseif ($inputPath[0] === '/') {
100 // Absolute path - prepend base path for subdirectory mounting
101 // e.g., url('/todos/') with baseUrl '/assistant' becomes '/assistant/todos/'
102 $resolvedPath = rtrim($basePath, '/') . $inputPath;
103 } else {
104 // Relative path - resolve against base path
105 // Append to base path directory
106 $baseDir = rtrim(dirname($basePath . '/dummy'), '/');
107 $resolvedPath = $baseDir . '/' . $inputPath;
108 }
109
110 // Normalize path (handle .. and .), preserving trailing slash
111 $hadTrailingSlash = str_ends_with($resolvedPath, '/');
112 $resolvedPath = (string) (new \mini\Util\Path($resolvedPath))->canonical();
113 if ($hadTrailingSlash && !str_ends_with($resolvedPath, '/')) {
114 $resolvedPath .= '/';
115 }
116
117 // Merge query parameters: input query + $query array
118 parse_str($inputQuery, $inputQueryParams);
119 $mergedQuery = http_build_query($inputQueryParams + $query);
120
121 // Build final URI
122 return $base
123 ->withPath($resolvedPath)
124 ->withQuery($mergedQuery)
125 ->withFragment($inputFragment);
126 }
127
128 /**
129 * Flash message functions
130 */
131 function flash_set($type, $message) {
132 if (!isset($_SESSION['flash'])) {
133 $_SESSION['flash'] = [];
134 }
135 $_SESSION['flash'][] = ['type' => $type, 'message' => $message];
136 }
137
138 /**
139 * Retrieve and clear all flash messages from the session
140 *
141 * Returns an array of flash messages and removes them from the session,
142 * ensuring each message is only displayed once.
143 *
144 * @return array<array{type: string, message: string}> Array of flash messages
145 */
146 function flash_get(): array {
147 if (!isset($_SESSION['flash'])) {
148 return [];
149 }
150 $flash = $_SESSION['flash'];
151 unset($_SESSION['flash']);
152 return $flash;
153 }
154
155
156 // Note: session(), db() and cache() helpers are now in src/ feature directories
157
158
159 /**
160 * Bootstrap the mini framework for controller files
161 *
162 * Call this at the top of any directly-accessible PHP file in the document root.
163 * Sets up error handling, output buffering, and clean URL redirects.
164 *
165 * Transitions application from Bootstrap to Ready phase, enabling access to Scoped services.
166 *
167 * Safe to call multiple times (idempotent after first call).
168 */
169 function bootstrap(): void
170 {
171 static $initialized = false;
172 if ($initialized) {
173 return; // Already bootstrapped
174 }
175 $initialized = true;
176
177 // Transition to Ready phase - enables request handling and access to Scoped services
178 Mini::$mini->phase->trigger(Phase::Ready);
179
180 // Clean up pre-existing output handlers
181 $previousLevel = -1;
182 while (ob_get_level() > 0 && ob_get_level() !== $previousLevel) {
183 $previousLevel = ob_get_level();
184 @ob_end_clean();
185 }
186
187 // Set up error handler (converts errors to exceptions)
188 set_error_handler(function($severity, $message, $file, $line) {
189 if (!(error_reporting() & $severity)) {
190 return false;
191 }
192 throw new \ErrorException($message, 0, $severity, $file, $line);
193 });
194
195 // Set up exception handler (fallback for bootstrap errors) - only if none exists
196 $existingHandler = set_exception_handler(null);
197 if ($existingHandler !== null) {
198 // Developer has their own exception handler - keep it
199 set_exception_handler($existingHandler);
200 } else {
201 // No handler exists - set Mini's fallback handler
202 // Note: When using dispatch(), Dispatcher handles exceptions during request lifecycle
203 set_exception_handler(function(\Throwable $exception) {
204 error_log("Uncaught exception: " . $exception->getMessage() . " in " . $exception->getFile() . " line " . $exception->getLine());
205 error_log("Stack trace: " . $exception->getTraceAsString());
206
207 if (headers_sent()) {
208 if (Mini::$mini->debug) {
209 echo $exception;
210 } else {
211 echo get_class($exception) . " thrown in " . $exception->getFile() . " line " . $exception->getLine();
212 }
213 die();
214 }
215
216 if (ob_get_level() > 0) {
217 ob_clean();
218 }
219
220 // Render basic error page
221 http_response_code(500);
222 echo "<h1>500 - Internal Server Error</h1>";
223 if (Mini::$mini->debug) {
224 echo "<pre>" . htmlspecialchars($exception->getMessage()) . "\n\n";
225 echo $exception->getTraceAsString() . "</pre>";
226 } else {
227 echo "<p>An unexpected error occurred.</p>";
228 }
229 });
230 }
231
232 // Start unlimited output buffering for exception recovery
233 // Buffer size 0 = unlimited, never auto-flush (prevents partial output on errors)
234 ob_start(null, 0);
235
236 // Parse application/json request bodies to $_POST (PHP doesn't do this natively)
237 if (str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'application/json')) {
238 $json = file_get_contents('php://input');
239 $data = json_decode($json, true);
240 if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
241 $_POST = $data;
242 }
243 }
244
245 // If routing is enabled and accessing /index.php directly, redirect to /
246 if (isset($GLOBALS['mini_routing_enabled']) && $GLOBALS['mini_routing_enabled']) {
247 $requestUri = $_SERVER['REQUEST_URI'] ?? '';
248 $path = parse_url($requestUri, PHP_URL_PATH) ?? '';
249 $query = $_SERVER['QUERY_STRING'] ?? '';
250
251 // Redirect /index.php to /
252 if ($path === '/index.php' || str_ends_with($path, '/index.php')) {
253 $redirectTo = rtrim(dirname($path), '/') ?: '/';
254 if ($query) {
255 $redirectTo .= '?' . $query;
256 }
257
258 http_response_code(301);
259 header('Location: ' . $redirectTo);
260 exit;
261 }
262 }
263 }
264
265 /**
266 * Router entry point for applications with routing enabled
267 *
268 * Call this from DOC_ROOT/index.php to enable routing:
269 * - Sets up error handling and output buffering
270 * - Delegates URL routing to Router
271 * - Routes loaded from _routes/ directory
272 *
273 * Route handlers in _routes/ don't need to call bootstrap().
274 */
275 // dispatch() function moved to src/Dispatcher/functions.php
276
277 /**
278 * Main entry point for file-based routing
279 *
280 * Bootstraps the application and routes the current request to
281 * the appropriate handler in the _routes/ directory.
282 *
283 * @deprecated Use dispatch() instead. Will be removed in future version.
284 */
285 function router(): void
286 {
287 // Set global flag that routing is enabled
288 $GLOBALS['mini_routing_enabled'] = true;
289
290 // Bootstrap sets up error handlers, output buffering, etc.
291 bootstrap();
292
293 // Delegate routing to Router
294 $requestUri = $_SERVER['REQUEST_URI'] ?? '';
295 $router = Mini::$mini->get(\mini\Router\Router::class);
296 $router->handleRequest($requestUri);
297
298 // Explicitly flush output buffer on successful completion
299 // (Exception handler discards buffer via ob_end_clean())
300 if (ob_get_level() > 0) {
301 ob_end_flush();
302 }
303 }
304
305
306 /**
307 * Create a CSRF token for a specific action
308 *
309 * Convenience wrapper around new CSRF().
310 *
311 * @param string $action Action name (e.g., 'delete-post', 'update-settings')
312 * @param string $fieldName HTML field name (default: '__nonce__')
313 * @return CSRF CSRF token object
314 */
315 function csrf(string $action, string $fieldName = '__nonce__'): CSRF {
316 return new CSRF($action, $fieldName);
317 }
318
319