Skip to content

Commit 9269ce9

Browse files
committed
feat: add helper route to enable routing with view files and outputing json directly
Signed-off-by: otengkwame <developerkwame@gmail.com>
1 parent 8e9f32e commit 9269ce9

2 files changed

Lines changed: 429 additions & 9 deletions

File tree

Core/core/Route/HelperRoute.php

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
<?php
2+
3+
namespace Base\Route;
4+
5+
use ArgumentCountError;
6+
use Closure;
7+
8+
class HelperRoute
9+
{
10+
11+
protected static $routes = [];
12+
protected static $currentUri = '';
13+
14+
/**
15+
* Initialize and check if we
16+
* should handle this request
17+
*/
18+
public static function init()
19+
{
20+
// Get the current URI
21+
self::$currentUri = self::getCurrentUri();
22+
23+
// Check if any of our routes match
24+
foreach (self::$routes as $pattern => $handler) {
25+
$params = self::matchRoute($pattern, self::$currentUri);
26+
27+
if ($params !== false) {
28+
// We have a match! Execute the handler
29+
self::executeHandler($handler, $params);
30+
exit; // Stop execution
31+
}
32+
}
33+
}
34+
35+
/**
36+
* Register a view route
37+
*
38+
* @param string $uri The URI pattern (e.g., 'about', 'profile/(:num)')
39+
* @param string $view The view file path
40+
* @param array $data Data to pass to the view
41+
*/
42+
public static function view($uri, $view, $data = [])
43+
{
44+
self::$routes[$uri] = [
45+
'type' => 'view',
46+
'view' => $view,
47+
'data' => $data
48+
];
49+
}
50+
51+
/**
52+
* Handle a view or json to be used in a custom route
53+
*
54+
* @param mixed $view
55+
* @param mixed $data
56+
* @return void
57+
*/
58+
public static function with($view = '', $data = [], $status = 200, $json = false)
59+
{
60+
61+
if ($json === true) {
62+
63+
http_response_code($status);
64+
header('Content-Type: application/json; charset=utf-8');
65+
66+
// Output JSON
67+
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
68+
return;
69+
}
70+
71+
HelperRoute::handleView($view, $data);
72+
}
73+
74+
/**
75+
* Register a JSON route
76+
*
77+
* @param string $uri The URI pattern
78+
* @param mixed $data Data to return (array or Closure)
79+
* @param int $status HTTP status code
80+
*/
81+
public static function json($uri, $data = [], $status = 200)
82+
{
83+
self::$routes[$uri] = [
84+
'type' => 'json',
85+
'data' => $data,
86+
'status' => $status
87+
];
88+
}
89+
90+
/**
91+
* Register a custom closure route
92+
*
93+
* @param string $uri The URI pattern
94+
* @param Closure $callback The callback to execute
95+
*/
96+
public static function closure($uri, ?Closure $callback = null)
97+
{
98+
self::$routes[$uri] = [
99+
'type' => 'closure',
100+
'callback' => $callback
101+
];
102+
}
103+
104+
/**
105+
* Get the current URI from the request
106+
*
107+
* @return string
108+
*/
109+
protected static function getCurrentUri()
110+
{
111+
// Get REQUEST_URI and clean it
112+
$uri = $_SERVER['REQUEST_URI'] ?? '';
113+
114+
// Remove query string
115+
if (($pos = strpos($uri, '?')) !== false) {
116+
$uri = substr($uri, 0, $pos);
117+
}
118+
119+
// Remove base path if CI is in a subdirectory
120+
$script_name = $_SERVER['SCRIPT_NAME'] ?? '';
121+
$base_path = dirname($script_name);
122+
123+
if ($base_path !== '/' && strpos($uri, $base_path) === 0) {
124+
$uri = substr($uri, strlen($base_path));
125+
}
126+
127+
// Remove index.php if present
128+
$uri = str_replace('/index.php', '', $uri);
129+
130+
// Clean up
131+
$uri = trim($uri, '/');
132+
133+
return $uri;
134+
}
135+
136+
/**
137+
* Match a route pattern against the current URI
138+
*
139+
* @param string $pattern The route pattern
140+
* @param string $uri The current URI
141+
* @return array|false Array of parameters or false if no match
142+
*/
143+
protected static function matchRoute($pattern, $uri)
144+
{
145+
// Trim slashes for comparison
146+
$pattern = trim($pattern, '/');
147+
$uri = trim($uri, '/');
148+
149+
// Exact match first (fastest check)
150+
if ($pattern === $uri) {
151+
return [];
152+
}
153+
154+
// Check if pattern is already a regex (starts with # or /)
155+
if (preg_match('/^[#\/]/', $pattern)) {
156+
// Already a regex pattern, use directly
157+
if (preg_match($pattern, $uri, $matches)) {
158+
array_shift($matches); // Remove full match
159+
return $matches;
160+
}
161+
return false;
162+
}
163+
164+
// Convert Laravel-style {placeholders} to regex
165+
$pattern = preg_replace_callback('/\{(\w+)\}/', function ($matches) {
166+
$placeholder = $matches[1];
167+
168+
// Map placeholder names to regex patterns
169+
switch (strtolower($placeholder)) {
170+
case 'id':
171+
case 'num':
172+
return '([0-9]+)';
173+
174+
case 'uuid':
175+
// UUID format: 8-4-4-4-12 hex characters
176+
return '([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})';
177+
178+
case 'alpha':
179+
return '([a-zA-Z]+)';
180+
181+
case 'alphanum':
182+
return '([a-zA-Z0-9]+)';
183+
184+
case 'slug':
185+
// Slug: lowercase alphanumeric with hyphens
186+
return '([a-z0-9-]+)';
187+
188+
case 'any':
189+
default:
190+
// Default to matching anything except /
191+
return '([^/]+)';
192+
}
193+
}, $pattern);
194+
195+
// Convert CI-style (:wildcards) to regex
196+
$replacements = [
197+
'(:any)' => '([^/]+)',
198+
'(:num)' => '([0-9]+)',
199+
'(:alpha)' => '([a-zA-Z]+)',
200+
'(:alphanum)' => '([a-zA-Z0-9]+)',
201+
];
202+
203+
$pattern = str_replace(array_keys($replacements), array_values($replacements), $pattern);
204+
205+
// Add regex delimiters and anchors
206+
$pattern = '#^' . $pattern . '$#';
207+
208+
// Try to match
209+
if (preg_match($pattern, $uri, $matches)) {
210+
// Remove the full match, keep only captured groups
211+
array_shift($matches);
212+
return $matches;
213+
}
214+
215+
return false;
216+
}
217+
218+
/**
219+
* Execute a route handler
220+
*
221+
* @param array $handler The handler configuration
222+
* @param array $params Route parameters
223+
*/
224+
protected static function executeHandler($handler, $params = [])
225+
{
226+
switch ($handler['type']) {
227+
case 'view':
228+
self::handleView($handler['view'], $handler['data']);
229+
break;
230+
231+
case 'json':
232+
self::handleJson($handler['data'], $handler['status'], $params);
233+
break;
234+
235+
case 'closure':
236+
self::handleClosure($handler['callback'], $params);
237+
break;
238+
}
239+
}
240+
241+
/**
242+
* Handle a view route
243+
*
244+
* @param string $view View file path
245+
* @param array $data Data for the view
246+
*/
247+
protected static function handleView($view, $data = [])
248+
{
249+
// Build the full path to the view
250+
$viewpath = VIEWPATH . $view . '.php';
251+
252+
if (!file_exists($viewpath)) {
253+
self::show404("View file not found: {$view}");
254+
return;
255+
}
256+
257+
// Extract data to make
258+
// variables available in view
259+
extract($data);
260+
261+
// Output the view
262+
require $viewpath;
263+
}
264+
265+
/**
266+
* Handle a JSON route
267+
*
268+
* @param mixed $data Data to output (array or Closure)
269+
* @param int $status HTTP status code
270+
* @param array $params Route parameters
271+
*/
272+
protected static function handleJson($data, $status, $params)
273+
{
274+
// If data is a closure, execute it with parameters
275+
if ($data instanceof Closure) {
276+
$data = call_user_func_array($data, $params);
277+
}
278+
279+
// Set headers
280+
http_response_code($status);
281+
header('Content-Type: application/json; charset=utf-8');
282+
283+
// Output JSON
284+
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
285+
}
286+
287+
/**
288+
* Handle a closure route
289+
*
290+
* @param Closure $callback The callback
291+
* @param array $params Route parameters
292+
*/
293+
protected static function handleClosure($callback, $params)
294+
{
295+
if (empty($params)) {
296+
throw new ArgumentCountError('Route::page or Route::json expects at least 1 parameter to be passed. E.g Route::page("uri/(:num)")');
297+
}
298+
299+
call_user_func_array($callback, $params);
300+
}
301+
302+
/**
303+
* Show a 404 page
304+
*
305+
* @param string $message Optional message
306+
*/
307+
protected static function show404($message = 'Page Not Found')
308+
{
309+
http_response_code(404);
310+
header('Location: ' . '404_Overwride');
311+
}
312+
313+
/**
314+
* Get all registered routes in
315+
* CodeIgniter 3 native route format (for debugging)
316+
* Returns clean, unique, readable routes like:
317+
*
318+
* [pages] => prefix-route/portfolio
319+
* [api/user/(:num)] => prefix-route/api/user/$1
320+
* [profile/(:any)] => prefix-route/profile/$1
321+
* [custom] => prefix-route/custom
322+
*
323+
* @return array
324+
*/
325+
public static function getRoutes()
326+
{
327+
$formatRoutes = [];
328+
$prefix = 'prefix-route';
329+
foreach (self::$routes as $uri => $handler) {
330+
$uri = $uri === '' ? '/' : $uri; // root fix
331+
332+
switch ($handler['type']) {
333+
case 'view':
334+
// For views: show the actual view name
335+
$target = $prefix . '/' . $handler['view'];
336+
break;
337+
338+
case 'json':
339+
case 'closure':
340+
// For json/closure: reflect the URI pattern with $1, $2, etc.
341+
$target = $prefix . '/' . $uri;
342+
343+
// Replace CI placeholders with $1, $2, etc. (like real CI routes)
344+
$placeholders = [
345+
'(:any)' => '$1',
346+
'(:num)' => '$1',
347+
'(:alpha)' => '$1',
348+
'(:alphanum)' => '$1',
349+
];
350+
351+
// Find all placeholders in order
352+
preg_match_all('#\((:[^)]+)\)#', $uri, $matches);
353+
$params = $matches[0]; // e.g. ['(:num)', '(:any)']
354+
355+
if (!empty($params)) {
356+
$target = $prefix . '/' . preg_replace(
357+
array_keys($placeholders),
358+
array_values($placeholders),
359+
$uri
360+
);
361+
362+
// Replace multiple placeholders: $1 → $2 → $3 etc.
363+
for ($i = 0; $i < count($params); $i++) {
364+
$target = str_replace('$1', '$' . ($i + 1), $target);
365+
}
366+
}
367+
break;
368+
369+
default:
370+
$target = $prefix . '/unknown';
371+
}
372+
373+
// Final cleanup: ensure no duplicate $1 after replacement
374+
$formatRoutes[$uri] = $target;
375+
}
376+
377+
return $formatRoutes;
378+
}
379+
}

0 commit comments

Comments
 (0)