create-prisma-php-app 1.28.8 → 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  declare(strict_types=1);
4
4
 
5
- if (session_status() == PHP_SESSION_NONE) {
5
+ if (session_status() === PHP_SESSION_NONE) {
6
6
  session_start();
7
7
  }
8
8
 
@@ -17,793 +17,781 @@ use Lib\Middleware\AuthMiddleware;
17
17
  use Lib\Auth\Auth;
18
18
  use Lib\MainLayout;
19
19
  use Lib\PHPX\TemplateCompiler;
20
+ use Lib\CacheHandler;
21
+ use Lib\ErrorHandler;
20
22
 
21
- $dotenv = Dotenv::createImmutable(\DOCUMENT_PATH);
22
- $dotenv->load();
23
+ final class Bootstrap
24
+ {
25
+ public static string $contentToInclude = '';
26
+ public static array $layoutsToInclude = [];
27
+ public static string $requestFilePath = '';
28
+ public static string $parentLayoutPath = '';
29
+ public static bool $isParentLayout = false;
30
+ public static bool $isContentIncluded = false;
31
+ public static bool $isChildContentIncluded = false;
32
+ public static bool $isContentVariableIncluded = false;
33
+ public static bool $secondRequestC69CD = false;
34
+ public static array $requestFilesData = [];
35
+
36
+ private static array $fileExistCache = [];
37
+ private static array $regexCache = [];
38
+
39
+ /**
40
+ * Main entry point to run the entire routing and rendering logic.
41
+ */
42
+ public static function run(): void
43
+ {
44
+ // Load environment variables
45
+ Dotenv::createImmutable(DOCUMENT_PATH)->load();
23
46
 
24
- PrismaPHPSettings::init();
25
- Request::init();
26
- StateManager::init();
47
+ // Set timezone
48
+ date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
27
49
 
28
- date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
50
+ // Initialize essential classes
51
+ PrismaPHPSettings::init();
52
+ Request::init();
53
+ StateManager::init();
29
54
 
30
- function determineContentToInclude()
31
- {
32
- /**
33
- * ============ URI Handling ============
34
- * The $requestUri variable now contains the full URI including query parameters.
35
- * Examples:
36
- * - Home page: '/'
37
- * - Dynamic routes with parameters (e.g., '/dashboard?v=2' or '/profile?id=5')
38
- * ======================================
39
- */
40
- $requestUri = $_SERVER['REQUEST_URI'];
41
- $requestUri = empty($_SERVER['SCRIPT_URL']) ? trim(uriExtractor($requestUri)) : trim($requestUri);
42
- /**
43
- * ============ URI Path Handling ============
44
- * The $uri variable now contains the URI path without query parameters and without the leading slash.
45
- * Examples:
46
- * - Home page: '' (empty string)
47
- * - Dynamic routes (e.g., '/dashboard?v=2' or '/profile?id=5') -> Only the path part is returned (e.g., 'dashboard' or 'profile'), without the query parameters.
48
- * ============================================
49
- */
50
- $scriptUrl = explode('?', $requestUri, 2)[0];
51
- $pathname = $_SERVER['SCRIPT_URL'] ?? $scriptUrl;
52
- $pathname = trim($pathname, '/');
53
- $baseDir = APP_PATH;
54
- $includePath = '';
55
- $layoutsToInclude = [];
56
-
57
- /**
58
- * ============ Middleware Management ============
59
- * AuthMiddleware is invoked to handle authentication logic for the current route ($pathname).
60
- * ================================================
61
- */
62
- AuthMiddleware::handle($pathname);
63
- /**
64
- * ============ End of Middleware Management ======
65
- * ================================================
66
- */
55
+ // Register custom handlers (exception, shutdown, error)
56
+ ErrorHandler::registerHandlers();
57
+
58
+ // Set a local store key as a cookie (before any output)
59
+ setcookie("pphp_local_store_key", PrismaPHPSettings::$localStoreKey, time() + 3600, "/", "", false, false);
60
+
61
+ $contentInfo = self::determineContentToInclude();
62
+ self::$contentToInclude = $contentInfo['path'] ?? '';
63
+ self::$layoutsToInclude = $contentInfo['layouts'] ?? [];
64
+
65
+ Request::$pathname = $contentInfo['pathname'] ? '/' . $contentInfo['pathname'] : '/';
66
+ Request::$uri = $contentInfo['uri'] ? $contentInfo['uri'] : '/';
67
+
68
+ if (is_file(self::$contentToInclude)) {
69
+ Request::$fileToInclude = basename(self::$contentToInclude);
70
+ }
71
+
72
+ if (self::fileExistsCached(self::$contentToInclude)) {
73
+ Request::$fileToInclude = basename(self::$contentToInclude);
74
+ }
75
+
76
+ self::checkForDuplicateRoutes();
77
+ self::authenticateUserToken();
78
+
79
+ self::$requestFilePath = APP_PATH . Request::$pathname;
80
+ self::$parentLayoutPath = APP_PATH . '/layout.php';
81
+
82
+ self::$isParentLayout = !empty(self::$layoutsToInclude)
83
+ && strpos(self::$layoutsToInclude[0], 'src/app/layout.php') !== false;
67
84
 
68
- $isDirectAccessToPrivateRoute = preg_match('/_/', $pathname);
69
- if ($isDirectAccessToPrivateRoute) {
70
- $sameSiteFetch = false;
71
- $serverFetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
72
- if (isset($serverFetchSite) && $serverFetchSite === 'same-origin') {
73
- $sameSiteFetch = true;
85
+ self::$isContentVariableIncluded = self::containsChildren(self::$parentLayoutPath);
86
+ if (!self::$isContentVariableIncluded) {
87
+ self::$isContentIncluded = true;
74
88
  }
75
89
 
76
- if (!$sameSiteFetch) {
77
- return ['path' => $includePath, 'layouts' => $layoutsToInclude, 'pathname' => $pathname, 'uri' => $requestUri];
90
+ self::$secondRequestC69CD = Request::$data['secondRequestC69CD'] ?? false;
91
+ self::$requestFilesData = PrismaPHPSettings::$includeFiles;
92
+
93
+ // Detect any fatal error that might have occurred before hitting this point
94
+ ErrorHandler::checkFatalError();
95
+ }
96
+
97
+ private static function fileExistsCached(string $path): bool
98
+ {
99
+ if (!isset(self::$fileExistCache[$path])) {
100
+ self::$fileExistCache[$path] = file_exists($path);
78
101
  }
102
+ return self::$fileExistCache[$path];
79
103
  }
80
104
 
81
- if ($pathname) {
82
- $groupFolder = findGroupFolder($pathname);
83
- if ($groupFolder) {
84
- $path = __DIR__ . $groupFolder;
85
- if (file_exists($path)) {
86
- $includePath = $path;
105
+ private static function pregMatchCached(string $pattern, string $subject): bool
106
+ {
107
+ $cacheKey = md5($pattern . $subject);
108
+ if (!isset(self::$regexCache[$cacheKey])) {
109
+ self::$regexCache[$cacheKey] = preg_match($pattern, $subject) === 1;
110
+ }
111
+ return self::$regexCache[$cacheKey];
112
+ }
113
+
114
+ private static function determineContentToInclude(): array
115
+ {
116
+ $requestUri = $_SERVER['REQUEST_URI'];
117
+ $requestUri = empty($_SERVER['SCRIPT_URL']) ? trim(self::uriExtractor($requestUri)) : trim($requestUri);
118
+
119
+ // Without query params
120
+ $scriptUrl = explode('?', $requestUri, 2)[0];
121
+ $pathname = $_SERVER['SCRIPT_URL'] ?? $scriptUrl;
122
+ $pathname = trim($pathname, '/');
123
+ $baseDir = APP_PATH;
124
+ $includePath = '';
125
+ $layoutsToInclude = [];
126
+
127
+ /**
128
+ * ============ Middleware Management ============
129
+ * AuthMiddleware is invoked to handle authentication logic for the current route ($pathname).
130
+ * ================================================
131
+ */
132
+ AuthMiddleware::handle($pathname);
133
+ /**
134
+ * ============ End of Middleware Management ======
135
+ * ================================================
136
+ */
137
+
138
+ // e.g., avoid direct access to _private files
139
+ $isDirectAccessToPrivateRoute = preg_match('/_/', $pathname);
140
+ if ($isDirectAccessToPrivateRoute) {
141
+ $sameSiteFetch = false;
142
+ $serverFetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '';
143
+ if (isset($serverFetchSite) && $serverFetchSite === 'same-origin') {
144
+ $sameSiteFetch = true;
145
+ }
146
+
147
+ if (!$sameSiteFetch) {
148
+ return [
149
+ 'path' => $includePath,
150
+ 'layouts' => $layoutsToInclude,
151
+ 'pathname' => $pathname,
152
+ 'uri' => $requestUri
153
+ ];
87
154
  }
88
155
  }
89
156
 
90
- if (empty($includePath)) {
91
- $dynamicRoute = dynamicRoute($pathname);
92
- if ($dynamicRoute) {
93
- $path = __DIR__ . $dynamicRoute;
94
- if (file_exists($path)) {
157
+ // Find matching route
158
+ if ($pathname) {
159
+ $groupFolder = self::findGroupFolder($pathname);
160
+ if ($groupFolder) {
161
+ $path = __DIR__ . $groupFolder;
162
+ if (self::fileExistsCached($path)) {
95
163
  $includePath = $path;
96
164
  }
97
165
  }
98
- }
99
-
100
- $currentPath = $baseDir;
101
- $getGroupFolder = getGroupFolder($groupFolder);
102
- $modifiedPathname = $pathname;
103
- if (!empty($getGroupFolder)) {
104
- $modifiedPathname = trim($getGroupFolder, "/src/app/");
105
- }
106
166
 
107
- foreach (explode('/', $modifiedPathname) as $segment) {
108
- if (empty($segment)) continue;
167
+ if (empty($includePath)) {
168
+ $dynamicRoute = self::dynamicRoute($pathname);
169
+ if ($dynamicRoute) {
170
+ $path = __DIR__ . $dynamicRoute;
171
+ if (self::fileExistsCached($path)) {
172
+ $includePath = $path;
173
+ }
174
+ }
175
+ }
109
176
 
110
- $currentPath .= '/' . $segment;
111
- $potentialLayoutPath = $currentPath . '/layout.php';
112
- if (file_exists($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude)) {
113
- $layoutsToInclude[] = $potentialLayoutPath;
177
+ // Check for layout hierarchy
178
+ $currentPath = $baseDir;
179
+ $getGroupFolder = self::getGroupFolder($groupFolder);
180
+ $modifiedPathname = $pathname;
181
+ if (!empty($getGroupFolder)) {
182
+ $modifiedPathname = trim($getGroupFolder, "/src/app/");
114
183
  }
115
- }
116
184
 
117
- if (isset($dynamicRoute)) {
118
- $currentDynamicPath = $baseDir;
119
- foreach (explode('/', $dynamicRoute) as $segment) {
120
- if (empty($segment)) continue;
185
+ foreach (explode('/', $modifiedPathname) as $segment) {
186
+ if (empty($segment)) {
187
+ continue;
188
+ }
121
189
 
122
- if ($segment === 'src' || $segment === 'app') continue;
190
+ $currentPath .= '/' . $segment;
191
+ $potentialLayoutPath = $currentPath . '/layout.php';
192
+ if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
193
+ $layoutsToInclude[] = $potentialLayoutPath;
194
+ }
195
+ }
123
196
 
124
- $currentDynamicPath .= '/' . $segment;
125
- $potentialDynamicRoute = $currentDynamicPath . '/layout.php';
126
- if (file_exists($potentialDynamicRoute) && !in_array($potentialDynamicRoute, $layoutsToInclude)) {
127
- $layoutsToInclude[] = $potentialDynamicRoute;
197
+ // If it was a dynamic route, we also check for any relevant layout
198
+ if (isset($dynamicRoute) && !empty($dynamicRoute)) {
199
+ $currentDynamicPath = $baseDir;
200
+ foreach (explode('/', $dynamicRoute) as $segment) {
201
+ if (empty($segment)) {
202
+ continue;
203
+ }
204
+ if ($segment === 'src' || $segment === 'app') {
205
+ continue;
206
+ }
207
+
208
+ $currentDynamicPath .= '/' . $segment;
209
+ $potentialDynamicRoute = $currentDynamicPath . '/layout.php';
210
+ if (self::fileExistsCached($potentialDynamicRoute) && !in_array($potentialDynamicRoute, $layoutsToInclude, true)) {
211
+ $layoutsToInclude[] = $potentialDynamicRoute;
212
+ }
128
213
  }
129
214
  }
130
- }
131
215
 
132
- if (empty($layoutsToInclude)) {
133
- $layoutsToInclude[] = $baseDir . '/layout.php';
216
+ // If still no layout, fallback to the app-level layout.php
217
+ if (empty($layoutsToInclude)) {
218
+ $layoutsToInclude[] = $baseDir . '/layout.php';
219
+ }
220
+ } else {
221
+ // If path is empty, we’re basically at "/"
222
+ $includePath = $baseDir . self::getFilePrecedence();
134
223
  }
135
- } else {
136
- $includePath = $baseDir . getFilePrecedence();
137
- }
138
224
 
139
- return ['path' => $includePath, 'layouts' => $layoutsToInclude, 'pathname' => $pathname, 'uri' => $requestUri];
140
- }
225
+ return [
226
+ 'path' => $includePath,
227
+ 'layouts' => $layoutsToInclude,
228
+ 'pathname' => $pathname,
229
+ 'uri' => $requestUri
230
+ ];
231
+ }
141
232
 
142
- function getFilePrecedence()
143
- {
144
- foreach (PrismaPHPSettings::$routeFiles as $route) {
145
- // Check if the file has a .php extension
146
- if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
147
- continue; // Skip files that are not PHP files
233
+ private static function getFilePrecedence(): ?string
234
+ {
235
+ foreach (PrismaPHPSettings::$routeFiles as $route) {
236
+ if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
237
+ continue;
238
+ }
239
+ if (preg_match('/^\.\/src\/app\/route\.php$/', $route)) {
240
+ return '/route.php';
241
+ }
242
+ if (preg_match('/^\.\/src\/app\/index\.php$/', $route)) {
243
+ return '/index.php';
244
+ }
148
245
  }
246
+ return null;
247
+ }
149
248
 
150
- // Check for route.php first
151
- if (preg_match('/^\.\/src\/app\/route\.php$/', $route)) {
152
- return '/route.php';
249
+ private static function uriExtractor(string $scriptUrl): string
250
+ {
251
+ $projectName = PrismaPHPSettings::$option->projectName ?? '';
252
+ if (empty($projectName)) {
253
+ return "/";
153
254
  }
154
255
 
155
- // If route.php is not found, check for index.php
156
- if (preg_match('/^\.\/src\/app\/index\.php$/', $route)) {
157
- return '/index.php';
256
+ $escapedIdentifier = preg_quote($projectName, '/');
257
+ if (preg_match("/(?:.*$escapedIdentifier)(\/.*)$/", $scriptUrl, $matches) && !empty($matches[1])) {
258
+ return rtrim(ltrim($matches[1], '/'), '/');
158
259
  }
159
- }
160
-
161
- // If neither file is found, return null
162
- return null;
163
- }
164
260
 
165
- function uriExtractor(string $scriptUrl): string
166
- {
167
- $projectName = PrismaPHPSettings::$option->projectName ?? '';
168
- if (empty($projectName)) {
169
261
  return "/";
170
262
  }
171
263
 
172
- $escapedIdentifier = preg_quote($projectName, '/');
173
- if (preg_match("/(?:.*$escapedIdentifier)(\/.*)$/", $scriptUrl, $matches) && !empty($matches[1])) {
174
- return rtrim(ltrim($matches[1], '/'), '/');
175
- }
176
-
177
- return "/";
178
- }
179
-
180
- function findGroupFolder($pathname): string
181
- {
182
- $pathnameSegments = explode('/', $pathname);
183
- foreach ($pathnameSegments as $segment) {
184
- if (!empty($segment)) {
185
- if (isGroupIdentifier($segment)) {
264
+ private static function findGroupFolder(string $pathname): string
265
+ {
266
+ $pathnameSegments = explode('/', $pathname);
267
+ foreach ($pathnameSegments as $segment) {
268
+ if (!empty($segment) && self::pregMatchCached('/^\(.*\)$/', $segment)) {
186
269
  return $segment;
187
270
  }
188
271
  }
189
- }
190
272
 
191
- $matchedGroupFolder = matchGroupFolder($pathname);
192
- if ($matchedGroupFolder) {
193
- return $matchedGroupFolder;
194
- } else {
195
- return '';
273
+ return self::matchGroupFolder($pathname) ?: '';
196
274
  }
197
- }
198
275
 
199
- function dynamicRoute($pathname)
200
- {
201
- $pathnameMatch = null;
202
- $normalizedPathname = ltrim(str_replace('\\', '/', $pathname), './');
203
- $normalizedPathnameEdited = "src/app/$normalizedPathname";
204
- $pathnameSegments = explode('/', $normalizedPathnameEdited);
276
+ private static function dynamicRoute($pathname)
277
+ {
278
+ $pathnameMatch = null;
279
+ $normalizedPathname = ltrim(str_replace('\\', '/', $pathname), './');
280
+ $normalizedPathnameEdited = "src/app/$normalizedPathname";
281
+ $pathnameSegments = explode('/', $normalizedPathnameEdited);
205
282
 
206
- foreach (PrismaPHPSettings::$routeFiles as $route) {
207
- $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
283
+ foreach (PrismaPHPSettings::$routeFiles as $route) {
284
+ $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
208
285
 
209
- // Skip non-.php files to improve performance
210
- if (pathinfo($normalizedRoute, PATHINFO_EXTENSION) !== 'php') {
211
- continue;
212
- }
286
+ if (pathinfo($normalizedRoute, PATHINFO_EXTENSION) !== 'php') {
287
+ continue;
288
+ }
213
289
 
214
- $routeSegments = explode('/', ltrim($normalizedRoute, '/'));
290
+ $routeSegments = explode('/', ltrim($normalizedRoute, '/'));
215
291
 
216
- $filteredRouteSegments = array_values(array_filter($routeSegments, function ($segment) {
217
- return !preg_match('/\(.+\)/', $segment); // Skip segments with parentheses (groups)
218
- }));
292
+ $filteredRouteSegments = array_values(array_filter($routeSegments, function ($segment) {
293
+ return !preg_match('/\(.+\)/', $segment);
294
+ }));
219
295
 
220
- $singleDynamic = preg_match_all('/\[[^\]]+\]/', $normalizedRoute, $matches) === 1 && strpos($normalizedRoute, '[...') === false;
296
+ $singleDynamic = (preg_match_all('/\[[^\]]+\]/', $normalizedRoute, $matches) === 1)
297
+ && strpos($normalizedRoute, '[...') === false;
298
+ $routeCount = count($filteredRouteSegments);
299
+ if (in_array(end($filteredRouteSegments), ['index.php', 'route.php'])) {
300
+ $expectedSegmentCount = $routeCount - 1;
301
+ } else {
302
+ $expectedSegmentCount = $routeCount;
303
+ }
221
304
 
222
- if ($singleDynamic) {
223
- $segmentMatch = singleDynamicRoute($pathnameSegments, $filteredRouteSegments);
224
- $index = array_search($segmentMatch, $filteredRouteSegments);
305
+ if ($singleDynamic) {
306
+ if (count($pathnameSegments) !== $expectedSegmentCount) {
307
+ continue;
308
+ }
225
309
 
226
- if ($index !== false && isset($pathnameSegments[$index])) {
227
- $trimSegmentMatch = trim($segmentMatch, '[]');
228
- Request::$dynamicParams = new \ArrayObject([$trimSegmentMatch => $pathnameSegments[$index]], \ArrayObject::ARRAY_AS_PROPS);
310
+ $segmentMatch = self::singleDynamicRoute($pathnameSegments, $filteredRouteSegments);
311
+ $index = array_search($segmentMatch, $filteredRouteSegments);
229
312
 
230
- $dynamicRoutePathname = str_replace($segmentMatch, $pathnameSegments[$index], $normalizedRoute);
231
- $dynamicRoutePathname = preg_replace('/\(.+\)/', '', $dynamicRoutePathname);
232
- $dynamicRoutePathname = preg_replace('/\/+/', '/', $dynamicRoutePathname);
233
- $dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
313
+ if ($index !== false && isset($pathnameSegments[$index])) {
314
+ $trimSegmentMatch = trim($segmentMatch, '[]');
315
+ Request::$dynamicParams = new ArrayObject(
316
+ [$trimSegmentMatch => $pathnameSegments[$index]],
317
+ ArrayObject::ARRAY_AS_PROPS
318
+ );
234
319
 
235
- $expectedPathname = rtrim('/src/app/' . $normalizedPathname, '/');
320
+ $dynamicRoutePathname = str_replace($segmentMatch, $pathnameSegments[$index], $normalizedRoute);
321
+ $dynamicRoutePathname = preg_replace('/\(.+\)/', '', $dynamicRoutePathname);
322
+ $dynamicRoutePathname = preg_replace('/\/+/', '/', $dynamicRoutePathname);
323
+ $dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
236
324
 
237
- if (strpos($normalizedRoute, 'route.php') !== false || strpos($normalizedRoute, 'index.php') !== false) {
238
- if ($expectedPathname === $dynamicRoutePathnameDirname) {
325
+ $expectedPathname = rtrim('/src/app/' . $normalizedPathname, '/');
326
+
327
+ if ((strpos($normalizedRoute, 'route.php') !== false || strpos($normalizedRoute, 'index.php') !== false)
328
+ && $expectedPathname === $dynamicRoutePathnameDirname
329
+ ) {
239
330
  $pathnameMatch = $normalizedRoute;
240
331
  break;
241
332
  }
242
333
  }
243
- }
244
- } elseif (strpos($normalizedRoute, '[...') !== false) {
245
- // Clean and normalize the route
246
- $cleanedNormalizedRoute = preg_replace('/\(.+\)/', '', $normalizedRoute);
247
- $cleanedNormalizedRoute = preg_replace('/\/+/', '/', $cleanedNormalizedRoute);
248
- $dynamicSegmentRoute = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedNormalizedRoute);
249
-
250
- // Check if the normalized pathname starts with the cleaned route
251
- if (strpos("/src/app/$normalizedPathname", $dynamicSegmentRoute) === 0) {
252
- $trimmedPathname = str_replace($dynamicSegmentRoute, '', "/src/app/$normalizedPathname");
253
- $pathnameParts = explode('/', trim($trimmedPathname, '/'));
254
-
255
- // Extract the dynamic segment content
256
- if (preg_match('/\[\.\.\.(.*?)\]/', $normalizedRoute, $matches)) {
257
- $dynamicParam = $matches[1];
258
- Request::$dynamicParams = new \ArrayObject([$dynamicParam => $pathnameParts], \ArrayObject::ARRAY_AS_PROPS);
334
+ } elseif (strpos($normalizedRoute, '[...') !== false) {
335
+ if (count($pathnameSegments) <= $expectedSegmentCount) {
336
+ continue;
259
337
  }
260
338
 
261
- // Check for 'route.php'
262
- if (strpos($normalizedRoute, 'route.php') !== false) {
263
- $pathnameMatch = $normalizedRoute;
264
- break;
265
- }
339
+ $cleanedNormalizedRoute = preg_replace('/\(.+\)/', '', $normalizedRoute);
340
+ $cleanedNormalizedRoute = preg_replace('/\/+/', '/', $cleanedNormalizedRoute);
341
+ $dynamicSegmentRoute = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedNormalizedRoute);
342
+
343
+ if (strpos("/src/app/$normalizedPathname", $dynamicSegmentRoute) === 0) {
344
+ $trimmedPathname = str_replace($dynamicSegmentRoute, '', "/src/app/$normalizedPathname");
345
+ $pathnameParts = explode('/', trim($trimmedPathname, '/'));
346
+
347
+ if (preg_match('/\[\.\.\.(.*?)\]/', $normalizedRoute, $matches)) {
348
+ $dynamicParam = $matches[1];
349
+ Request::$dynamicParams = new ArrayObject(
350
+ [$dynamicParam => $pathnameParts],
351
+ ArrayObject::ARRAY_AS_PROPS
352
+ );
353
+ }
354
+
355
+ if (strpos($normalizedRoute, 'route.php') !== false) {
356
+ $pathnameMatch = $normalizedRoute;
357
+ break;
358
+ }
266
359
 
267
- // Handle matching routes ending with 'index.php'
268
- if (strpos($normalizedRoute, 'index.php') !== false) {
269
- $segmentMatch = "[...$dynamicParam]";
270
- $index = array_search($segmentMatch, $filteredRouteSegments);
360
+ if (strpos($normalizedRoute, 'index.php') !== false) {
361
+ $segmentMatch = "[...$dynamicParam]";
362
+ $index = array_search($segmentMatch, $filteredRouteSegments);
271
363
 
272
- if ($index !== false && isset($pathnameSegments[$index])) {
273
- // Generate the dynamic pathname
274
- $dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
275
- $dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
364
+ if ($index !== false && isset($pathnameSegments[$index])) {
365
+ $dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
366
+ $dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
276
367
 
277
- $expectedPathname = rtrim("/src/app/$normalizedPathname", '/');
368
+ $expectedPathname = rtrim("/src/app/$normalizedPathname", '/');
278
369
 
279
- // Compare the expected and dynamic pathname
280
- if ($expectedPathname === $dynamicRoutePathnameDirname) {
281
- $pathnameMatch = $normalizedRoute;
282
- break;
370
+ if ($expectedPathname === $dynamicRoutePathnameDirname) {
371
+ $pathnameMatch = $normalizedRoute;
372
+ break;
373
+ }
283
374
  }
284
375
  }
285
376
  }
286
377
  }
287
378
  }
288
- }
289
-
290
- return $pathnameMatch;
291
- }
292
379
 
293
- function isGroupIdentifier($segment): bool
294
- {
295
- return (bool)preg_match('/^\(.*\)$/', $segment);
296
- }
297
-
298
- function matchGroupFolder($constructedPath): ?string
299
- {
300
- $bestMatch = null;
301
- $normalizedConstructedPath = ltrim(str_replace('\\', '/', $constructedPath), './');
380
+ return $pathnameMatch;
381
+ }
302
382
 
303
- $routeFile = "/src/app/$normalizedConstructedPath/route.php";
304
- $indexFile = "/src/app/$normalizedConstructedPath/index.php";
383
+ private static function matchGroupFolder(string $constructedPath): ?string
384
+ {
385
+ $bestMatch = null;
386
+ $normalizedConstructedPath = ltrim(str_replace('\\', '/', $constructedPath), './');
387
+ $routeFile = "/src/app/$normalizedConstructedPath/route.php";
388
+ $indexFile = "/src/app/$normalizedConstructedPath/index.php";
305
389
 
306
- foreach (PrismaPHPSettings::$routeFiles as $route) {
307
- if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
308
- continue;
390
+ foreach (PrismaPHPSettings::$routeFiles as $route) {
391
+ if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
392
+ continue;
393
+ }
394
+ $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
395
+ $cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
396
+
397
+ if ($cleanedRoute === $routeFile) {
398
+ $bestMatch = $normalizedRoute;
399
+ break;
400
+ } elseif ($cleanedRoute === $indexFile && !$bestMatch) {
401
+ $bestMatch = $normalizedRoute;
402
+ }
309
403
  }
310
404
 
311
- $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
312
-
313
- $cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
314
- if ($cleanedRoute === $routeFile) {
315
- $bestMatch = $normalizedRoute;
316
- break;
317
- } elseif ($cleanedRoute === $indexFile && !$bestMatch) {
318
- $bestMatch = $normalizedRoute;
319
- }
405
+ return $bestMatch;
320
406
  }
321
407
 
322
- return $bestMatch;
323
- }
408
+ private static function getGroupFolder($pathname): string
409
+ {
410
+ $lastSlashPos = strrpos($pathname, '/');
411
+ if ($lastSlashPos === false) {
412
+ return "";
413
+ }
324
414
 
325
- function getGroupFolder($pathname): string
326
- {
327
- $lastSlashPos = strrpos($pathname, '/');
415
+ $pathWithoutFile = substr($pathname, 0, $lastSlashPos);
416
+ if (preg_match('/\(([^)]+)\)[^()]*$/', $pathWithoutFile, $matches)) {
417
+ return $pathWithoutFile;
418
+ }
328
419
 
329
- if ($lastSlashPos === false) {
330
420
  return "";
331
421
  }
332
422
 
333
- $pathWithoutFile = substr($pathname, 0, $lastSlashPos);
334
-
335
- if (preg_match('/\(([^)]+)\)[^()]*$/', $pathWithoutFile, $matches)) {
336
- return $pathWithoutFile;
337
- }
338
-
339
- return "";
340
- }
341
-
342
- function singleDynamicRoute($pathnameSegments, $routeSegments)
343
- {
344
- $segmentMatch = "";
345
- foreach ($routeSegments as $index => $segment) {
346
- if (preg_match('/^\[[^\]]+\]$/', $segment)) {
347
- return "{$segment}";
348
- } else {
349
- if ($segment !== $pathnameSegments[$index]) {
350
- return $segmentMatch;
423
+ private static function singleDynamicRoute($pathnameSegments, $routeSegments)
424
+ {
425
+ $segmentMatch = "";
426
+ foreach ($routeSegments as $index => $segment) {
427
+ if (preg_match('/^\[[^\]]+\]$/', $segment)) {
428
+ return $segment;
429
+ } else {
430
+ if (!isset($pathnameSegments[$index]) || $segment !== $pathnameSegments[$index]) {
431
+ return $segmentMatch;
432
+ }
351
433
  }
352
434
  }
435
+ return $segmentMatch;
353
436
  }
354
- return $segmentMatch;
355
- }
356
437
 
357
- function checkForDuplicateRoutes()
358
- {
359
- if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'production') return;
360
-
361
- $normalizedRoutesMap = [];
362
- foreach (PrismaPHPSettings::$routeFiles as $route) {
363
- if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
364
- continue;
438
+ private static function checkForDuplicateRoutes(): void
439
+ {
440
+ // Skip checks in production
441
+ if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'production') {
442
+ return;
365
443
  }
366
444
 
367
- $routeWithoutGroups = preg_replace('/\(.*?\)/', '', $route);
368
- $routeTrimmed = ltrim($routeWithoutGroups, '.\\/');
369
- $routeTrimmed = preg_replace('#/{2,}#', '/', $routeTrimmed);
370
- $routeTrimmed = preg_replace('#\\\\{2,}#', '\\', $routeTrimmed);
371
- $routeNormalized = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $routeTrimmed);
372
- $normalizedRoutesMap[$routeNormalized][] = $route;
373
- }
445
+ $normalizedRoutesMap = [];
446
+ foreach (PrismaPHPSettings::$routeFiles as $route) {
447
+ if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
448
+ continue;
449
+ }
374
450
 
375
- $errorMessages = [];
376
- foreach ($normalizedRoutesMap as $normalizedRoute => $originalRoutes) {
377
- $basename = basename($normalizedRoute);
378
- if ($basename === 'layout.php') continue;
451
+ $routeWithoutGroups = preg_replace('/\(.*?\)/', '', $route);
452
+ $routeTrimmed = ltrim($routeWithoutGroups, '.\\/');
453
+ $routeTrimmed = preg_replace('#/{2,}#', '/', $routeTrimmed);
454
+ $routeTrimmed = preg_replace('#\\\\{2,}#', '\\', $routeTrimmed);
455
+ $routeNormalized = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $routeTrimmed);
379
456
 
380
- if (count($originalRoutes) > 1 && strpos($normalizedRoute, DIRECTORY_SEPARATOR) !== false) {
381
- if ($basename !== 'route.php' && $basename !== 'index.php') continue;
457
+ $normalizedRoutesMap[$routeNormalized][] = $route;
458
+ }
382
459
 
383
- $errorMessages[] = "Duplicate route found after normalization: " . $normalizedRoute;
460
+ $errorMessages = [];
461
+ foreach ($normalizedRoutesMap as $normalizedRoute => $originalRoutes) {
462
+ $basename = basename($normalizedRoute);
463
+ if ($basename === 'layout.php') {
464
+ continue;
465
+ }
384
466
 
385
- foreach ($originalRoutes as $originalRoute) {
386
- $errorMessages[] = "- Grouped original route: " . $originalRoute;
467
+ if (
468
+ count($originalRoutes) > 1 &&
469
+ strpos($normalizedRoute, DIRECTORY_SEPARATOR) !== false
470
+ ) {
471
+ if ($basename !== 'route.php' && $basename !== 'index.php') {
472
+ continue;
473
+ }
474
+ $errorMessages[] = "Duplicate route found after normalization: " . $normalizedRoute;
475
+ foreach ($originalRoutes as $originalRoute) {
476
+ $errorMessages[] = "- Grouped original route: " . $originalRoute;
477
+ }
387
478
  }
388
479
  }
389
- }
390
480
 
391
- if (!empty($errorMessages)) {
392
- if (isAjaxOrXFileRequestOrRouteFile()) {
393
- $errorMessageString = implode("\n", $errorMessages);
394
- } else {
395
- $errorMessageString = implode("<br>", $errorMessages);
481
+ if (!empty($errorMessages)) {
482
+ $errorMessageString = self::isAjaxOrXFileRequestOrRouteFile()
483
+ ? implode("\n", $errorMessages)
484
+ : implode("<br>", $errorMessages);
485
+
486
+ ErrorHandler::modifyOutputLayoutForError($errorMessageString);
396
487
  }
397
- modifyOutputLayoutForError($errorMessageString);
398
488
  }
399
- }
400
-
401
- function containsChildLayoutChildren($filePath)
402
- {
403
- $fileContent = file_get_contents($filePath);
404
489
 
405
- // Updated regular expression to match MainLayout::$childLayoutChildren
406
- $pattern = '/\<\?=\s*MainLayout::\$childLayoutChildren\s*;?\s*\?>|echo\s*MainLayout::\$childLayoutChildren\s*;?/';
490
+ public static function containsChildLayoutChildren($filePath): bool
491
+ {
492
+ if (!self::fileExistsCached($filePath)) {
493
+ return false;
494
+ }
407
495
 
408
- // Return true if MainLayout::$childLayoutChildren variables are found, false otherwise
409
- return preg_match($pattern, $fileContent) === 1;
410
- }
496
+ $fileContent = @file_get_contents($filePath);
497
+ if ($fileContent === false) {
498
+ return false;
499
+ }
411
500
 
412
- function containsChildren($filePath)
413
- {
414
- $fileContent = file_get_contents($filePath);
501
+ // Check usage of MainLayout::$childLayoutChildren
502
+ $pattern = '/\<\?=\s*MainLayout::\$childLayoutChildren\s*;?\s*\?>|echo\s*MainLayout::\$childLayoutChildren\s*;?/';
503
+ return (bool) preg_match($pattern, $fileContent);
504
+ }
415
505
 
416
- // Updated regular expression to match MainLayout::$children
417
- $pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
418
- // Return true if the new content variables are found, false otherwise
419
- return preg_match($pattern, $fileContent) === 1;
420
- }
506
+ private static function containsChildren($filePath): bool
507
+ {
508
+ if (!self::fileExistsCached($filePath)) {
509
+ return false;
510
+ }
421
511
 
422
- function convertToArrayObject($data)
423
- {
424
- if (is_array($data)) {
425
- $arrayObject = new \ArrayObject([], \ArrayObject::ARRAY_AS_PROPS);
426
- foreach ($data as $key => $value) {
427
- $arrayObject[$key] = convertToArrayObject($value);
512
+ $fileContent = @file_get_contents($filePath);
513
+ if ($fileContent === false) {
514
+ return false;
428
515
  }
429
- return $arrayObject;
430
- }
431
- return $data;
432
- }
433
516
 
434
- function wireCallback()
435
- {
436
- try {
437
- // Initialize response
438
- $response = [
439
- 'success' => false,
440
- 'error' => 'Callback not provided',
441
- 'data' => null
442
- ];
517
+ // Check usage of MainLayout::$children
518
+ $pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
519
+ return (bool) preg_match($pattern, $fileContent);
520
+ }
443
521
 
444
- $callbackResponse = null;
445
- $data = [];
446
-
447
- // Check if the request includes one or more files
448
- $hasFile = isset($_FILES['file']) && !empty($_FILES['file']['name'][0]);
449
-
450
- // Process form data
451
- if ($hasFile) {
452
- // Handle file upload, including multiple files
453
- $data = $_POST; // Form data will be available in $_POST
454
-
455
- if (is_array($_FILES['file']['name'])) {
456
- // Multiple files uploaded
457
- $files = [];
458
- foreach ($_FILES['file']['name'] as $index => $name) {
459
- $files[] = [
460
- 'name' => $name,
461
- 'type' => $_FILES['file']['type'][$index],
462
- 'tmp_name' => $_FILES['file']['tmp_name'][$index],
463
- 'error' => $_FILES['file']['error'][$index],
464
- 'size' => $_FILES['file']['size'][$index],
465
- ];
466
- }
467
- $data['files'] = $files;
468
- } else {
469
- // Single file uploaded
470
- $data['file'] = $_FILES['file']; // Attach single file information to data
471
- }
472
- } else {
473
- // Handle non-file form data (likely JSON)
474
- $input = file_get_contents('php://input');
475
- $data = json_decode($input, true);
522
+ private static function convertToArrayObject($data)
523
+ {
524
+ return is_array($data) ? (object) $data : $data;
525
+ }
476
526
 
477
- if (json_last_error() !== JSON_ERROR_NONE) {
478
- // Fallback to handle form data in POST (non-JSON)
479
- $data = $_POST;
480
- }
481
- }
527
+ /**
528
+ * Used specifically for wire (AJAX) calls.
529
+ * Ends execution with JSON response.
530
+ */
531
+ public static function wireCallback()
532
+ {
533
+ try {
534
+ // Initialize response
535
+ $response = [
536
+ 'success' => false,
537
+ 'error' => 'Callback not provided',
538
+ 'data' => null
539
+ ];
482
540
 
483
- // Validate and call the dynamic function
484
- if (isset($data['callback'])) {
485
- // Sanitize and create a dynamic function name
486
- $callbackName = preg_replace('/[^a-zA-Z0-9_]/', '', $data['callback']); // Sanitize the callback name
541
+ $callbackResponse = null;
542
+ $data = [];
487
543
 
488
- // Check if the dynamic function is defined and callable
489
- if (function_exists($callbackName) && is_callable($callbackName)) {
490
- $dataObject = convertToArrayObject($data);
544
+ // Check if the request includes one or more files
545
+ $hasFile = isset($_FILES['file']) && !empty($_FILES['file']['name'][0]);
491
546
 
492
- // Call the function dynamically
493
- $callbackResponse = call_user_func($callbackName, $dataObject);
547
+ // Process form data
548
+ if ($hasFile) {
549
+ $data = $_POST;
494
550
 
495
- // Handle different types of responses
496
- if (is_string($callbackResponse) || is_bool($callbackResponse)) {
497
- // Prepare success response
498
- $response = [
499
- 'success' => true,
500
- 'response' => $callbackResponse
501
- ];
551
+ if (is_array($_FILES['file']['name'])) {
552
+ $files = [];
553
+ foreach ($_FILES['file']['name'] as $index => $name) {
554
+ $files[] = [
555
+ 'name' => $name,
556
+ 'type' => $_FILES['file']['type'][$index],
557
+ 'tmp_name' => $_FILES['file']['tmp_name'][$index],
558
+ 'error' => $_FILES['file']['error'][$index],
559
+ 'size' => $_FILES['file']['size'][$index],
560
+ ];
561
+ }
562
+ $data['files'] = $files;
502
563
  } else {
503
- // Handle non-string, non-boolean responses
504
- $response = [
505
- 'success' => true,
506
- 'response' => $callbackResponse
507
- ];
564
+ $data['file'] = $_FILES['file'];
508
565
  }
509
566
  } else {
510
- if ($callbackName === 'appState_59E13') {
511
- $response = [
512
- 'success' => true,
513
- 'response' => 'localStorage updated'
514
- ];
567
+ $input = file_get_contents('php://input');
568
+ $data = json_decode($input, true);
569
+
570
+ if (json_last_error() !== JSON_ERROR_NONE) {
571
+ $data = $_POST;
572
+ }
573
+ }
574
+
575
+ // Validate and call the dynamic function
576
+ if (isset($data['callback'])) {
577
+ $callbackName = preg_replace('/[^a-zA-Z0-9_]/', '', $data['callback']);
578
+
579
+ if (function_exists($callbackName) && is_callable($callbackName)) {
580
+ $dataObject = self::convertToArrayObject($data);
581
+ // Call the function
582
+ $callbackResponse = call_user_func($callbackName, $dataObject);
583
+
584
+ if (is_string($callbackResponse) || is_bool($callbackResponse)) {
585
+ $response = [
586
+ 'success' => true,
587
+ 'response' => $callbackResponse
588
+ ];
589
+ } else {
590
+ $response = [
591
+ 'success' => true,
592
+ 'response' => $callbackResponse
593
+ ];
594
+ }
515
595
  } else {
516
- $response['error'] = 'Invalid callback';
596
+ if ($callbackName === PrismaPHPSettings::$localStoreKey) {
597
+ $response = [
598
+ 'success' => true,
599
+ 'response' => 'localStorage updated'
600
+ ];
601
+ } else {
602
+ $response['error'] = 'Invalid callback';
603
+ }
517
604
  }
605
+ } else {
606
+ $response['error'] = 'No callback provided';
518
607
  }
519
- } else {
520
- $response['error'] = 'No callback provided';
521
- }
522
608
 
523
- // Output the JSON response only if the callbackResponse is not null
524
- if ($callbackResponse !== null) {
525
- echo json_encode($response);
526
- } else {
527
- if (isset($response['error'])) {
609
+ // Output the JSON response only if the callbackResponse is not null
610
+ if ($callbackResponse !== null || isset($response['error'])) {
528
611
  echo json_encode($response);
529
612
  }
613
+ } catch (Throwable $e) {
614
+ $response = [
615
+ 'success' => false,
616
+ 'error' => 'Exception occurred',
617
+ 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
618
+ 'file' => htmlspecialchars($e->getFile(), ENT_QUOTES, 'UTF-8'),
619
+ 'line' => (int) $e->getLine()
620
+ ];
621
+
622
+ echo json_encode($response, JSON_UNESCAPED_UNICODE);
530
623
  }
531
- } catch (Throwable $e) {
532
- // Handle any exceptions and prepare an error response
533
- $response = [
534
- 'success' => false,
535
- 'error' => 'Exception occurred',
536
- 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
537
- 'file' => htmlspecialchars($e->getFile(), ENT_QUOTES, 'UTF-8'),
538
- 'line' => $e->getLine()
539
- ];
540
624
 
541
- // Output the error response
542
- echo json_encode($response);
625
+ exit;
543
626
  }
544
627
 
545
- exit;
546
- }
628
+ public static function getLoadingsFiles(): string
629
+ {
630
+ // Gather all loading.php files
631
+ $loadingFiles = array_filter(PrismaPHPSettings::$routeFiles, function ($route) {
632
+ $normalizedRoute = str_replace('\\', '/', $route);
633
+ return preg_match('/\/loading\.php$/', $normalizedRoute);
634
+ });
547
635
 
548
- function getLoadingsFiles()
549
- {
550
- $loadingFiles = array_filter(PrismaPHPSettings::$routeFiles, function ($route) {
551
- $normalizedRoute = str_replace('\\', '/', $route);
552
- return preg_match('/\/loading\.php$/', $normalizedRoute);
553
- });
636
+ $haveLoadingFileContent = array_reduce($loadingFiles, function ($carry, $route) {
637
+ $normalizeUri = str_replace('\\', '/', $route);
638
+ $fileUrl = str_replace('./src/app', '', $normalizeUri);
639
+ $route = str_replace(['\\', './'], ['/', ''], $route);
554
640
 
555
- $haveLoadingFileContent = array_reduce($loadingFiles, function ($carry, $route) {
556
- $normalizeUri = str_replace('\\', '/', $route);
557
- $fileUrl = str_replace('./src/app', '', $normalizeUri);
558
- $route = str_replace(['\\', './'], ['/', ''], $route);
559
-
560
- ob_start();
561
- include($route);
562
- $loadingContent = ob_get_clean();
641
+ ob_start();
642
+ include($route);
643
+ $loadingContent = ob_get_clean();
644
+
645
+ if ($loadingContent !== false) {
646
+ $url = $fileUrl === '/loading.php'
647
+ ? '/'
648
+ : str_replace('/loading.php', '', $fileUrl);
649
+ $carry .= '<div pp-loading-url="' . $url . '">' . $loadingContent . '</div>';
650
+ }
651
+ return $carry;
652
+ }, '');
563
653
 
564
- if ($loadingContent !== false) {
565
- $url = $fileUrl === '/loading.php' ? '/' : str_replace('/loading.php', '', $fileUrl);
566
- $carry .= '<div pp-loading-url="' . $url . '">' . $loadingContent . '</div>';
654
+ if ($haveLoadingFileContent) {
655
+ return '<div style="display: none;" id="loading-file-1B87E">' . $haveLoadingFileContent . '</div>';
567
656
  }
568
-
569
- return $carry;
570
- }, '');
571
-
572
- if ($haveLoadingFileContent) {
573
- return '<div style="display: none;" id="loading-file-1B87E">' . $haveLoadingFileContent . '</div>';
657
+ return '';
574
658
  }
575
659
 
576
- return '';
577
- }
578
-
579
- function modifyOutputLayoutForError($contentToAdd)
580
- {
581
- $errorFile = APP_PATH . '/error.php';
582
- $errorFileExists = file_exists($errorFile);
660
+ public static function createUpdateRequestData(): void
661
+ {
662
+ $requestJsonData = SETTINGS_PATH . '/request-data.json';
583
663
 
584
- if ($_ENV['SHOW_ERRORS'] === "false") {
585
- if ($errorFileExists) {
586
- if (isAjaxOrXFileRequestOrRouteFile()) {
587
- $contentToAdd = "An error occurred";
588
- } else {
589
- $contentToAdd = "<div class='error'>An error occurred</div>";
590
- }
664
+ if (file_exists($requestJsonData)) {
665
+ $currentData = json_decode(file_get_contents($requestJsonData), true) ?? [];
591
666
  } else {
592
- exit; // Exit if SHOW_ERRORS is false and no error file exists
667
+ $currentData = [];
593
668
  }
594
- }
595
669
 
596
- if ($errorFileExists) {
670
+ $includedFiles = get_included_files();
671
+ $srcAppFiles = [];
672
+ foreach ($includedFiles as $filename) {
673
+ if (strpos($filename, DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR) !== false) {
674
+ $srcAppFiles[] = $filename;
675
+ }
676
+ }
597
677
 
598
- $errorContent = $contentToAdd;
678
+ $currentUrl = urldecode(Request::$uri);
599
679
 
600
- if (isAjaxOrXFileRequestOrRouteFile()) {
601
- header('Content-Type: application/json');
602
- echo json_encode([
603
- 'success' => false,
604
- 'error' => $errorContent
605
- ]);
606
- http_response_code(403);
607
- } else {
608
- $layoutFile = APP_PATH . '/layout.php';
609
- if (file_exists($layoutFile)) {
680
+ if (isset($currentData[$currentUrl])) {
681
+ $currentData[$currentUrl]['includedFiles'] = array_values(array_unique(
682
+ array_merge($currentData[$currentUrl]['includedFiles'], $srcAppFiles)
683
+ ));
610
684
 
611
- ob_start();
612
- require_once $errorFile;
613
- MainLayout::$children = ob_get_clean();
614
- require_once $layoutFile;
615
- } else {
616
- echo $errorContent;
685
+ if (!Request::$isWire && !self::$secondRequestC69CD) {
686
+ $currentData[$currentUrl]['isCacheable'] = CacheHandler::$isCacheable;
617
687
  }
618
- }
619
- } else {
620
- if (isAjaxOrXFileRequestOrRouteFile()) {
621
- header('Content-Type: application/json');
622
- echo json_encode([
623
- 'success' => false,
624
- 'error' => $contentToAdd
625
- ]);
626
- http_response_code(403);
627
688
  } else {
628
- echo $contentToAdd;
689
+ $currentData[$currentUrl] = [
690
+ 'url' => $currentUrl,
691
+ 'fileName' => self::convertUrlToFileName($currentUrl),
692
+ 'isCacheable' => CacheHandler::$isCacheable,
693
+ 'cacheTtl' => CacheHandler::$ttl,
694
+ 'includedFiles' => $srcAppFiles,
695
+ ];
629
696
  }
630
- }
631
- exit;
632
- }
633
-
634
- function createUpdateRequestData()
635
- {
636
- $requestJsonData = SETTINGS_PATH . '/request-data.json';
637
697
 
638
- // Check if the JSON file exists
639
- if (file_exists($requestJsonData)) {
640
- // Read the current data from the JSON file
641
- $currentData = json_decode(file_get_contents($requestJsonData), true);
642
- } else {
643
- // If the file doesn't exist, initialize an empty array
644
- $currentData = [];
645
- }
698
+ $existingData = file_exists($requestJsonData) ? file_get_contents($requestJsonData) : '';
699
+ $newData = json_encode($currentData, JSON_PRETTY_PRINT);
646
700
 
647
- // Get the list of included/required files
648
- $includedFiles = get_included_files();
649
-
650
- // Filter only the files inside the src/app directory
651
- $srcAppFiles = [];
652
- foreach ($includedFiles as $filename) {
653
- if (strpos($filename, DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR) !== false) {
654
- $srcAppFiles[] = $filename;
701
+ if ($existingData !== $newData) {
702
+ file_put_contents($requestJsonData, $newData);
655
703
  }
656
704
  }
657
705
 
658
- // Extract the current request URL
659
- $currentUrl = Request::$uri;
660
-
661
- // If the URL already exists in the data, merge new included files with the existing ones
662
- if (isset($currentData[$currentUrl])) {
663
- // Merge the existing and new included files, removing duplicates
664
- $currentData[$currentUrl]['includedFiles'] = array_unique(
665
- array_merge($currentData[$currentUrl]['includedFiles'], $srcAppFiles)
666
- );
667
- } else {
668
- // If the URL doesn't exist, add a new entry
669
- $currentData[$currentUrl] = [
670
- 'url' => $currentUrl,
671
- 'includedFiles' => $srcAppFiles,
672
- ];
706
+ private static function convertUrlToFileName(string $url): string
707
+ {
708
+ $url = trim($url, '/');
709
+ $fileName = preg_replace('/[^a-zA-Z0-9-_]/', '_', $url);
710
+ return $fileName ?: 'index';
673
711
  }
674
712
 
675
- // Convert the array back to JSON and save it to the file
676
- $jsonData = json_encode($currentData, JSON_PRETTY_PRINT);
677
- file_put_contents($requestJsonData, $jsonData);
678
- }
679
-
680
- function authenticateUserToken()
681
- {
682
- $token = Request::getBearerToken();
683
- if ($token) {
684
- $auth = Auth::getInstance();
685
- $verifyToken = $auth->verifyToken($token);
686
- if ($verifyToken) {
687
- $auth->signIn($verifyToken);
713
+ private static function authenticateUserToken(): void
714
+ {
715
+ $token = Request::getBearerToken();
716
+ if ($token) {
717
+ $auth = Auth::getInstance();
718
+ $verifyToken = $auth->verifyToken($token);
719
+ if ($verifyToken) {
720
+ $auth->signIn($verifyToken);
721
+ }
688
722
  }
689
723
  }
690
- }
691
-
692
- function isAjaxOrXFileRequestOrRouteFile(): bool
693
- {
694
- if (Request::$fileToInclude === 'index.php') {
695
- return false;
696
- }
697
724
 
698
- return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
699
- }
700
-
701
- set_exception_handler(function ($exception) {
702
- if (isAjaxOrXFileRequestOrRouteFile()) {
703
- $errorContent = "Exception: " . $exception->getMessage();
704
- } else {
705
- $errorContent = "<div class='error'>Exception: " . htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8') . "</div>";
706
- }
707
- modifyOutputLayoutForError($errorContent);
708
- });
709
-
710
- register_shutdown_function(function () {
711
- $error = error_get_last();
712
- if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_RECOVERABLE_ERROR])) {
713
-
714
- if (isAjaxOrXFileRequestOrRouteFile()) {
715
- $errorContent = "Fatal Error: " . $error['message'] . " in " . $error['file'] . " on line " . $error['line'];
716
- } else {
717
- $errorContent = "<div class='error'>Fatal Error: " . htmlspecialchars($error['message'], ENT_QUOTES, 'UTF-8') .
718
- " in " . htmlspecialchars($error['file'], ENT_QUOTES, 'UTF-8') .
719
- " on line " . $error['line'] . "</div>";
725
+ public static function isAjaxOrXFileRequestOrRouteFile(): bool
726
+ {
727
+ if (Request::$fileToInclude === 'index.php') {
728
+ return false;
720
729
  }
721
- modifyOutputLayoutForError($errorContent);
722
- }
723
- });
724
730
 
725
- try {
726
- $_determineContentToInclude = determineContentToInclude();
727
- $_contentToInclude = $_determineContentToInclude['path'] ?? '';
728
- $_layoutsToInclude = $_determineContentToInclude['layouts'] ?? [];
729
- Request::$pathname = $_determineContentToInclude['pathname'] ? '/' . $_determineContentToInclude['pathname'] : '/';
730
- Request::$uri = $_determineContentToInclude['uri'] ? $_determineContentToInclude['uri'] : '/';
731
- if (is_file($_contentToInclude)) {
732
- Request::$fileToInclude = basename($_contentToInclude); // returns the file name
731
+ return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
733
732
  }
733
+ }
734
734
 
735
- checkForDuplicateRoutes();
736
- authenticateUserToken();
735
+ // ============================================================================
736
+ // Main Execution
737
+ // ============================================================================
738
+ Bootstrap::run();
737
739
 
738
- if (empty($_contentToInclude)) {
740
+ try {
741
+ // 1) If there's no content to include:
742
+ if (empty(Bootstrap::$contentToInclude)) {
739
743
  if (!Request::$isXFileRequest && PrismaPHPSettings::$option->backendOnly) {
740
- // Set the header and output a JSON response for permission denied
741
744
  header('Content-Type: application/json');
742
745
  echo json_encode([
743
746
  'success' => false,
744
747
  'error' => 'Permission denied'
745
748
  ]);
746
- http_response_code(403); // Set HTTP status code to 403 Forbidden
749
+ http_response_code(403);
747
750
  exit;
748
751
  }
749
752
 
750
- $_requestFilePath = APP_PATH . Request::$pathname;
751
- if (is_file($_requestFilePath)) {
752
- if (file_exists($_requestFilePath) && Request::$isXFileRequest) {
753
- // Check if the file is a PHP file
754
- if (pathinfo($_requestFilePath, PATHINFO_EXTENSION) === 'php') {
755
- // Include the PHP file without setting the JSON header
756
- include $_requestFilePath;
753
+ // If the file physically exists on disk and we’re dealing with an X-File request
754
+ if (is_file(Bootstrap::$requestFilePath)) {
755
+ if (file_exists(Bootstrap::$requestFilePath) && Request::$isXFileRequest) {
756
+ if (pathinfo(Bootstrap::$requestFilePath, PATHINFO_EXTENSION) === 'php') {
757
+ include Bootstrap::$requestFilePath;
757
758
  } else {
758
- // Set the appropriate content-type for non-PHP files if needed
759
- // and read the content
760
- header('Content-Type: ' . mime_content_type($_requestFilePath)); // Dynamic content type
761
- readfile($_requestFilePath);
759
+ header('Content-Type: ' . mime_content_type(Bootstrap::$requestFilePath));
760
+ readfile(Bootstrap::$requestFilePath);
762
761
  }
763
762
  exit;
764
763
  }
765
764
  } else if (PrismaPHPSettings::$option->backendOnly) {
766
- // Set the header and output a JSON response for file not found
767
765
  header('Content-Type: application/json');
768
- echo json_encode([
769
- 'success' => false,
770
- 'error' => 'Not found'
771
- ]);
772
- http_response_code(404); // Set HTTP status code to 404 Not Found
773
- exit;
766
+ http_response_code(404);
767
+ exit(json_encode(['success' => false, 'error' => 'Not found']));
774
768
  }
775
769
  }
776
770
 
777
- if (!empty($_contentToInclude) && Request::$fileToInclude === 'route.php') {
771
+ // 2) If the chosen file is route.php -> output JSON
772
+ if (!empty(Bootstrap::$contentToInclude) && Request::$fileToInclude === 'route.php') {
778
773
  header('Content-Type: application/json');
779
- require_once $_contentToInclude;
774
+ require_once Bootstrap::$contentToInclude;
780
775
  exit;
781
776
  }
782
777
 
783
- $_parentLayoutPath = APP_PATH . '/layout.php';
784
- $_isParentLayout = !empty($_layoutsToInclude) && strpos($_layoutsToInclude[0], 'src/app/layout.php') !== false;
785
-
786
- $_isContentIncluded = false;
787
- $_isChildContentIncluded = false;
788
- $_isContentVariableIncluded = containsChildren($_parentLayoutPath);
789
- if (!$_isContentVariableIncluded) {
790
- $_isContentIncluded = true;
791
- }
792
-
793
- if (!empty($_contentToInclude) && !empty(Request::$fileToInclude)) {
794
- if (!$_isParentLayout) {
778
+ // 3) If there is some valid content (index.php or something else)
779
+ if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
780
+ // We only load the content now if we're NOT dealing with the top-level parent layout
781
+ if (!Bootstrap::$isParentLayout) {
795
782
  ob_start();
796
- require_once $_contentToInclude;
783
+ require_once Bootstrap::$contentToInclude;
797
784
  MainLayout::$childLayoutChildren = ob_get_clean();
798
785
  }
799
- foreach (array_reverse($_layoutsToInclude) as $layoutPath) {
800
- if ($_parentLayoutPath === $layoutPath) {
786
+
787
+ // Then process all the reversed layouts in the chain
788
+ foreach (array_reverse(Bootstrap::$layoutsToInclude) as $layoutPath) {
789
+ if (Bootstrap::$parentLayoutPath === $layoutPath) {
801
790
  continue;
802
791
  }
803
792
 
804
- $_isChildContentVariableIncluded = containsChildLayoutChildren($layoutPath);
805
- if (!$_isChildContentVariableIncluded) {
806
- $_isChildContentIncluded = true;
793
+ if (!Bootstrap::containsChildLayoutChildren($layoutPath)) {
794
+ Bootstrap::$isChildContentIncluded = true;
807
795
  }
808
796
 
809
797
  ob_start();
@@ -811,31 +799,36 @@ try {
811
799
  MainLayout::$childLayoutChildren = ob_get_clean();
812
800
  }
813
801
  } else {
802
+ // Fallback: we include not-found.php
814
803
  ob_start();
815
804
  require_once APP_PATH . '/not-found.php';
816
805
  MainLayout::$childLayoutChildren = ob_get_clean();
817
806
  }
818
807
 
819
- if ($_isParentLayout && !empty($_contentToInclude)) {
808
+ // If the top-level layout is in use
809
+ if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
820
810
  ob_start();
821
- require_once $_contentToInclude;
811
+ require_once Bootstrap::$contentToInclude;
822
812
  MainLayout::$childLayoutChildren = ob_get_clean();
823
813
  }
824
814
 
825
- if (!$_isContentIncluded && !$_isChildContentIncluded) {
826
- $_secondRequestC69CD = Request::$data['secondRequestC69CD'] ?? false;
827
-
828
- if (!$_secondRequestC69CD) {
829
- createUpdateRequestData();
815
+ if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
816
+ // Provide request-data for SSR caching, if needed
817
+ if (!Bootstrap::$secondRequestC69CD) {
818
+ Bootstrap::createUpdateRequestData();
830
819
  }
831
820
 
832
- if (Request::$isWire && !$_secondRequestC69CD) {
833
- $_requestFilesData = PrismaPHPSettings::$includeFiles;
834
-
835
- if (isset($_requestFilesData[Request::$uri])) {
836
- $_requestDataToLoop = $_requestFilesData[Request::$uri];
821
+ // If there’s caching
822
+ if (isset(Bootstrap::$requestFilesData[Request::$uri])) {
823
+ if ($_ENV['CACHE_ENABLED'] === 'true') {
824
+ CacheHandler::serveCache(Request::$uri, intval($_ENV['CACHE_TTL']));
825
+ }
826
+ }
837
827
 
838
- foreach ($_requestDataToLoop['includedFiles'] as $file) {
828
+ // For wire calls, re-include the files if needed
829
+ if (Request::$isWire && !Bootstrap::$secondRequestC69CD) {
830
+ if (isset(Bootstrap::$requestFilesData[Request::$uri])) {
831
+ foreach (Bootstrap::$requestFilesData[Request::$uri]['includedFiles'] as $file) {
839
832
  if (file_exists($file)) {
840
833
  ob_start();
841
834
  require_once $file;
@@ -845,77 +838,54 @@ try {
845
838
  }
846
839
  }
847
840
 
848
- MainLayout::$children = MainLayout::$childLayoutChildren;
849
- MainLayout::$children .= getLoadingsFiles();
850
- MainLayout::$children = TemplateCompiler::compile(MainLayout::$children);
841
+ // If it’s a wire request, handle wire callback
842
+ if (Request::$isWire && !Bootstrap::$secondRequestC69CD) {
843
+ ob_end_clean();
844
+ Bootstrap::wireCallback();
845
+ }
846
+
847
+ MainLayout::$children = MainLayout::$childLayoutChildren . Bootstrap::getLoadingsFiles();
851
848
 
852
849
  ob_start();
853
850
  require_once APP_PATH . '/layout.php';
851
+ MainLayout::$html = ob_get_clean();
852
+ MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
853
+ MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
854
+ MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
854
855
 
855
- if (Request::$isWire && !$_secondRequestC69CD) {
856
- ob_end_clean();
857
- wireCallback();
858
- } else {
859
- echo ob_get_clean();
856
+ if (isset(Bootstrap::$requestFilesData[Request::$uri]['fileName']) && $_ENV['CACHE_ENABLED'] === 'true') {
857
+ CacheHandler::saveCache(Request::$uri, MainLayout::$html);
860
858
  }
859
+
860
+ echo MainLayout::$html;
861
861
  } else {
862
- if ($_isContentIncluded) {
863
- if (isAjaxOrXFileRequestOrRouteFile()) {
864
- $_errorDetails = "The layout file does not contain &lt;?php echo MainLayout::\$childLayoutChildren; ?&gt; or &lt;?= MainLayout::\$childLayoutChildren ?&gt;<br><strong>$layoutPath</strong>";
865
- } else {
866
- $_errorDetails = "<div class='error'>The parent layout file does not contain &lt;?php echo MainLayout::\$children; ?&gt; Or &lt;?= MainLayout::\$children ?&gt;<br>" . "<strong>$_parentLayoutPath</strong></div>";
867
- }
868
- modifyOutputLayoutForError($_errorDetails);
869
- } else {
870
- if (isAjaxOrXFileRequestOrRouteFile()) {
871
- $_errorDetails = "The layout file does not contain &lt;?php echo MainLayout::\$childLayoutChildren; ?&gt; or &lt;?= MainLayout::\$childLayoutChildren ?&gt;<br><strong>$layoutPath</strong>";
872
- } else {
873
- $_errorDetails = "<div class='error'>The layout file does not contain &lt;?php echo MainLayout::\$childLayoutChildren; ?&gt; or &lt;?= MainLayout::\$childLayoutChildren ?&gt;<br><strong>$layoutPath</strong></div>";
874
- }
875
- modifyOutputLayoutForError($_errorDetails);
876
- }
877
- }
878
- } catch (Throwable $e) {
879
- if (isAjaxOrXFileRequestOrRouteFile()) {
880
- $_errorDetails = "Unhandled Exception: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine();
881
- } else {
882
- $_errorDetails = "Unhandled Exception: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
883
- $_errorDetails .= "<br>File: " . htmlspecialchars($e->getFile(), ENT_QUOTES, 'UTF-8');
884
- $_errorDetails .= "<br>Line: " . htmlspecialchars((string)$e->getLine(), ENT_QUOTES, 'UTF-8');
885
- $_errorDetails .= "<br/>TraceAsString: " . htmlspecialchars($e->getTraceAsString(), ENT_QUOTES, 'UTF-8');
886
- $_errorDetails = "<div class='error'>$_errorDetails</div>";
887
- }
888
- modifyOutputLayoutForError($_errorDetails);
889
- }
862
+ $layoutPath = Bootstrap::$isContentIncluded
863
+ ? Bootstrap::$parentLayoutPath
864
+ : (Bootstrap::$layoutsToInclude[0] ?? '');
890
865
 
891
- (function () {
892
- $lastErrorCapture = error_get_last();
893
- if ($lastErrorCapture !== null) {
866
+ $message = "The layout file does not contain <?= MainLayout::\$childLayoutChildren ?>\n<strong>$layoutPath</strong>";
867
+ $htmlMessage = "<div class='error'>The layout file does not contain <?= MainLayout::\$childLayoutChildren ?><br><strong>$layoutPath</strong></div>";
894
868
 
895
- if (isAjaxOrXFileRequestOrRouteFile()) {
896
- $errorContent = "Error: " . $lastErrorCapture['message'] . " in " . $lastErrorCapture['file'] . " on line " . $lastErrorCapture['line'];
897
- } else {
898
- $errorContent = "<div class='error'>Error: " . $lastErrorCapture['message'] . " in " . $lastErrorCapture['file'] . " on line " . $lastErrorCapture['line'] . "</div>";
869
+ if (Bootstrap::$isContentIncluded) {
870
+ $message = "The parent layout file does not contain <?= MainLayout::\$children ?> or <?= MainLayout::\$childLayoutChildren ?><br><strong>$layoutPath</strong>";
871
+ $htmlMessage = "<div class='error'>The parent layout file does not contain <?= MainLayout::\$children ?> or <?= MainLayout::\$childLayoutChildren ?><br><strong>$layoutPath</strong></div>";
899
872
  }
900
- modifyOutputLayoutForError($errorContent);
901
- }
902
- })();
903
873
 
904
- set_error_handler(function ($severity, $message, $file, $line) {
905
- if (!(error_reporting() & $severity)) {
906
- // This error code is not included in error_reporting
907
- return;
908
- }
874
+ $errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
909
875
 
910
- // Capture the specific severity types, including warnings (E_WARNING)
911
- if (isAjaxOrXFileRequestOrRouteFile()) {
912
- $errorContent = "Error: {$severity} - {$message} in {$file} on line {$line}";
913
- } else {
914
- $errorContent = "<div class='error'>Error: {$message} in {$file} on line {$line}</div>";
876
+ ErrorHandler::modifyOutputLayoutForError($errorDetails);
915
877
  }
916
-
917
- // If needed, log it or output immediately based on severity
918
- if ($severity === E_WARNING || $severity === E_NOTICE) {
919
- modifyOutputLayoutForError($errorContent);
878
+ } catch (Throwable $e) {
879
+ if (Bootstrap::isAjaxOrXFileRequestOrRouteFile()) {
880
+ $errorDetails = "Unhandled Exception: " . $e->getMessage() .
881
+ " in " . $e->getFile() .
882
+ " on line " . $e->getLine();
883
+ } else {
884
+ $errorDetails = "Unhandled Exception: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
885
+ $errorDetails .= "<br>File: " . htmlspecialchars($e->getFile(), ENT_QUOTES, 'UTF-8');
886
+ $errorDetails .= "<br>Line: " . htmlspecialchars((string)$e->getLine(), ENT_QUOTES, 'UTF-8');
887
+ $errorDetails .= "<br/>TraceAsString: " . htmlspecialchars($e->getTraceAsString(), ENT_QUOTES, 'UTF-8');
888
+ $errorDetails = "<div class='error'>{$errorDetails}</div>";
920
889
  }
921
- });
890
+ ErrorHandler::modifyOutputLayoutForError($errorDetails);
891
+ }