Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
phphr
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
phphr
Commits
2ed51510
Commit
2ed51510
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 4 files via Son of Anton
parent
ee11d13c
Pipeline
#20
canceled with stage
Changes
4
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
248 additions
and
60 deletions
+248
-60
app.php
bootstrap/app.php
+52
-25
App.php
engine/Core/App.php
+54
-16
Router.php
engine/Core/Router.php
+116
-14
index.php
public/index.php
+26
-5
No files found.
bootstrap/app.php
View file @
2ed51510
...
@@ -120,19 +120,20 @@ $container->singleton(JobRunner::class, function () use ($container) {
...
@@ -120,19 +120,20 @@ $container->singleton(JobRunner::class, function () use ($container) {
return
new
JobRunner
(
$container
->
resolve
(
Connection
::
class
));
return
new
JobRunner
(
$container
->
resolve
(
Connection
::
class
));
});
});
// Router
// Router
— use the static singleton so route files can call Router::get() etc.
$container
->
singleton
(
Router
::
class
,
fn
()
=>
new
Router
());
$container
->
singleton
(
Router
::
class
,
fn
()
=>
Router
::
getInstance
());
// App
// App
$container
->
singleton
(
App
::
class
,
function
()
use
(
$container
)
{
$container
->
singleton
(
App
::
class
,
function
()
use
(
$container
)
{
return
new
App
(
$container
);
return
new
App
(
$container
);
});
});
//
Register Event Listeners
//
─── Register Event Listeners ───
$dispatcher
=
$container
->
resolve
(
EventDispatcher
::
class
);
$dispatcher
=
$container
->
resolve
(
EventDispatcher
::
class
);
$listenerRegistrar
=
ROOT_PATH
.
'/config/event_listeners.php'
;
$listenerRegistrar
=
ROOT_PATH
.
'/config/event_listeners.php'
;
if
(
file_exists
(
$listenerRegistrar
))
{
if
(
file_exists
(
$listenerRegistrar
))
{
$listeners
=
require
$listenerRegistrar
;
$listeners
=
require
$listenerRegistrar
;
if
(
is_array
(
$listeners
))
{
foreach
(
$listeners
as
$event
=>
$handlers
)
{
foreach
(
$listeners
as
$event
=>
$handlers
)
{
foreach
(
$handlers
as
$handler
)
{
foreach
(
$handlers
as
$handler
)
{
$dispatcher
->
listen
(
$event
,
function
(
$payload
)
use
(
$handler
,
$container
)
{
$dispatcher
->
listen
(
$event
,
function
(
$payload
)
use
(
$handler
,
$container
)
{
...
@@ -141,35 +142,61 @@ if (file_exists($listenerRegistrar)) {
...
@@ -141,35 +142,61 @@ if (file_exists($listenerRegistrar)) {
});
});
}
}
}
}
}
}
}
//
Register Calculators
//
─── Register Calculators (silently skip if classes don't exist) ───
$calcEngine
=
$container
->
resolve
(
CalculationEngine
::
class
);
$calcEngine
=
$container
->
resolve
(
CalculationEngine
::
class
);
$calculators
=
ROOT_PATH
.
'/config/calculators.php'
;
$calculators
File
=
ROOT_PATH
.
'/config/calculators.php'
;
if
(
file_exists
(
$calculators
))
{
if
(
file_exists
(
$calculators
File
))
{
$calcs
=
require
$calculators
;
$calcs
=
require
$calculators
File
;
foreach
(
$calcs
as
$name
=>
$className
)
{
foreach
(
$calcs
as
$name
=>
$className
)
{
try
{
if
(
class_exists
(
$className
))
{
$calcEngine
->
register
(
$name
,
$container
->
resolve
(
$className
));
$calcEngine
->
register
(
$name
,
$container
->
resolve
(
$className
));
}
}
}
catch
(
\Throwable
$e
)
{
error_log
(
"[Bootstrap] Failed to register calculator '
{
$name
}
':
{
$e
->
getMessage
()
}
"
);
}
}
}
}
//
Register Scheduled Jobs
//
─── Register Scheduled Jobs (silently skip if classes don't exist) ───
$jobRunner
=
$container
->
resolve
(
JobRunner
::
class
);
$jobRunner
=
$container
->
resolve
(
JobRunner
::
class
);
$jobs
=
ROOT_PATH
.
'/config/scheduled_jobs.php'
;
$jobs
File
=
ROOT_PATH
.
'/config/scheduled_jobs.php'
;
if
(
file_exists
(
$jobs
))
{
if
(
file_exists
(
$jobs
File
))
{
$jobDefs
=
require
$jobs
;
$jobDefs
=
require
$jobs
File
;
foreach
(
$jobDefs
as
$key
=>
$className
)
{
foreach
(
$jobDefs
as
$key
=>
$className
)
{
try
{
if
(
class_exists
(
$className
))
{
$jobRunner
->
register
(
$key
,
$container
->
resolve
(
$className
));
$jobRunner
->
register
(
$key
,
$container
->
resolve
(
$className
));
}
}
}
catch
(
\Throwable
$e
)
{
error_log
(
"[Bootstrap] Failed to register job '
{
$key
}
':
{
$e
->
getMessage
()
}
"
);
}
}
}
}
//
Load all module routes
//
─── Load All Module Routes ───
$router
=
$container
->
resolve
(
Router
::
class
);
$router
=
$container
->
resolve
(
Router
::
class
);
$moduleDir
=
ROOT_PATH
.
'/modules'
;
$moduleDir
=
ROOT_PATH
.
'/modules'
;
$modules
=
array_filter
(
glob
(
$moduleDir
.
'/*'
),
'is_dir'
);
if
(
is_dir
(
$moduleDir
))
{
foreach
(
$modules
as
$modulePath
)
{
$modules
=
array_filter
(
glob
(
$moduleDir
.
'/*'
),
'is_dir'
);
sort
(
$modules
);
foreach
(
$modules
as
$modulePath
)
{
$routeFile
=
$modulePath
.
'/routes.php'
;
$routeFile
=
$modulePath
.
'/routes.php'
;
if
(
file_exists
(
$routeFile
))
{
if
(
file_exists
(
$routeFile
))
{
require
$routeFile
;
try
{
$result
=
require
$routeFile
;
// Handle route files that return a callable (BoardTemplates, CardTemplates pattern)
if
(
is_callable
(
$result
))
{
$result
(
$router
);
}
}
catch
(
\Throwable
$e
)
{
$moduleName
=
basename
(
$modulePath
);
error_log
(
"[Bootstrap] Failed to load routes for module '
{
$moduleName
}
':
{
$e
->
getMessage
()
}
in
{
$e
->
getFile
()
}
:
{
$e
->
getLine
()
}
"
);
}
}
}
}
}
}
\ No newline at end of file
engine/Core/App.php
View file @
2ed51510
...
@@ -36,10 +36,17 @@ final class App
...
@@ -36,10 +36,17 @@ final class App
$router
=
$this
->
container
->
resolve
(
Router
::
class
);
$router
=
$this
->
container
->
resolve
(
Router
::
class
);
try
{
try
{
// Handle root redirect to /login
if
(
$request
->
uri
()
===
'/'
||
$request
->
uri
()
===
''
)
{
$response
=
Response
::
redirect
(
'/login'
);
$response
->
send
();
return
;
}
$route
=
$router
->
match
(
$request
);
$route
=
$router
->
match
(
$request
);
if
(
$route
===
null
)
{
if
(
$route
===
null
)
{
$this
->
sendError
(
404
,
'Not Found'
);
$this
->
sendError
(
404
,
'Not Found'
,
$request
);
return
;
return
;
}
}
...
@@ -55,14 +62,9 @@ final class App
...
@@ -55,14 +62,9 @@ final class App
$response
->
send
();
$response
->
send
();
}
catch
(
\Throwable
$e
)
{
}
catch
(
\Throwable
$e
)
{
$debug
=
$this
->
container
->
resolve
(
Config
::
class
)
->
get
(
'app.debug'
,
false
);
error_log
(
"[App Error]
{
$e
->
getMessage
()
}
in
{
$e
->
getFile
()
}
:
{
$e
->
getLine
()
}
"
);
error_log
(
"[App Error]
{
$e
->
getMessage
()
}
in
{
$e
->
getFile
()
}
:
{
$e
->
getLine
()
}
"
);
if
(
$debug
)
{
$this
->
sendError
(
500
,
$e
->
getMessage
(),
$request
);
$this
->
sendError
(
500
,
$e
->
getMessage
()
.
"
\n
"
.
$e
->
getTraceAsString
());
}
else
{
$this
->
sendError
(
500
,
'Internal Server Error'
);
}
}
}
}
}
...
@@ -72,10 +74,14 @@ final class App
...
@@ -72,10 +74,14 @@ final class App
$method
=
$route
[
'method'
];
$method
=
$route
[
'method'
];
$params
=
$route
[
'params'
]
??
[];
$params
=
$route
[
'params'
]
??
[];
if
(
!
class_exists
(
$controllerClass
))
{
return
Response
::
json
([
'error'
=>
"Controller not found:
{
$controllerClass
}
"
],
500
);
}
$controller
=
$this
->
container
->
resolve
(
$controllerClass
);
$controller
=
$this
->
container
->
resolve
(
$controllerClass
);
if
(
!
method_exists
(
$controller
,
$method
))
{
if
(
!
method_exists
(
$controller
,
$method
))
{
return
Response
::
json
([
'error'
=>
'Method not found'
],
404
);
return
Response
::
json
([
'error'
=>
"Method
{
$method
}
not found on
{
$controllerClass
}
"
],
404
);
}
}
$result
=
$controller
->
$method
(
$request
,
...
$params
);
$result
=
$controller
->
$method
(
$request
,
...
$params
);
...
@@ -87,20 +93,52 @@ final class App
...
@@ -87,20 +93,52 @@ final class App
return
Response
::
json
(
$result
);
return
Response
::
json
(
$result
);
}
}
private
function
sendError
(
int
$code
,
string
$message
)
:
void
private
function
sendError
(
int
$code
,
string
$message
,
?
Request
$request
=
null
)
:
void
{
{
http_response_code
(
$code
);
http_response_code
(
$code
);
if
(
str_contains
(
$_SERVER
[
'HTTP_ACCEPT'
]
??
''
,
'application/json'
))
{
$wantsJson
=
$request
&&
(
str_contains
(
$request
->
header
(
'accept'
,
''
),
'json'
)
||
str_contains
(
$request
->
header
(
'content-type'
,
''
),
'json'
)
||
str_starts_with
(
$request
->
uri
(),
'/api/'
)
);
if
(
$wantsJson
)
{
header
(
'Content-Type: application/json'
);
header
(
'Content-Type: application/json'
);
echo
json_encode
([
'error'
=>
$message
,
'code'
=>
$code
]);
echo
json_encode
([
'error'
=>
$message
,
'code'
=>
$code
]);
}
else
{
}
else
{
// Try template, fall back to inline HTML
try
{
$templateEngine
=
$this
->
container
->
resolve
(
\Engine\Template\TemplateEngine
::
class
);
$templateEngine
=
$this
->
container
->
resolve
(
\Engine\Template\TemplateEngine
::
class
);
$templateFile
=
ROOT_PATH
.
"/templates/errors/
{
$code
}
.php"
;
$templateFile
=
ROOT_PATH
.
"/templates/errors/
{
$code
}
.php"
;
if
(
file_exists
(
$templateFile
))
{
if
(
file_exists
(
$templateFile
))
{
echo
$templateEngine
->
render
(
"errors/
{
$code
}
"
,
[
'message'
=>
$message
]);
echo
$templateEngine
->
render
(
"errors/
{
$code
}
"
,
[
'message'
=>
$message
]);
}
else
{
return
;
echo
"<h1>
{
$code
}
</h1><p>"
.
htmlspecialchars
(
$message
)
.
"</p>"
;
}
}
catch
(
\Throwable
$e
)
{
// Template engine failed, use inline HTML
}
}
echo
<<<HTML
<!DOCTYPE html>
<html>
<head><title>{$code} Error</title>
<style>
body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}
.box{text-align:center;max-width:600px;padding:40px}
h1{font-size:5em;margin:0;color:#6366f1}
p{color:#94a3b8;margin-top:16px}
pre{background:#1e293b;padding:16px;border-radius:8px;text-align:left;overflow-x:auto;font-size:0.85em;margin-top:20px;white-space:pre-wrap}
a{color:#6366f1;text-decoration:none}
</style>
</head>
<body><div class="box">
<h1>{$code}</h1>
<p>{$message}</p>
<pre>{$message}</pre>
<p style="margin-top:24px"><a href="/login">← Go to Login</a></p>
</div></body></html>
HTML;
}
}
}
}
}
}
\ No newline at end of file
engine/Core/Router.php
View file @
2ed51510
...
@@ -5,37 +5,116 @@ namespace Engine\Core;
...
@@ -5,37 +5,116 @@ namespace Engine\Core;
final
class
Router
final
class
Router
{
{
private
static
?
self
$instance
=
null
;
private
array
$routes
=
[];
private
array
$routes
=
[];
private
array
$groupStack
=
[];
private
array
$groupStack
=
[];
public
function
get
(
string
$uri
,
string
$controller
,
string
$method
)
:
self
/**
* Middleware alias map — short names to full class names.
*/
private
static
array
$middlewareAliases
=
[
'auth'
=>
\Middleware\AuthenticationMiddleware
::
class
,
'audit'
=>
\Middleware\AuditMiddleware
::
class
,
'json_body'
=>
\Middleware\JsonBodyParserMiddleware
::
class
,
'api_key_auth'
=>
\Middleware\ApiKeyAuthMiddleware
::
class
,
'cors'
=>
\Middleware\CORSMiddleware
::
class
,
'csrf'
=>
\Middleware\CSRFMiddleware
::
class
,
'blocking'
=>
\Middleware\BlockingNotificationMiddleware
::
class
,
'security'
=>
\Middleware\SecurityHeadersMiddleware
::
class
,
];
public
function
__construct
()
{
if
(
self
::
$instance
===
null
)
{
self
::
$instance
=
$this
;
}
}
public
static
function
getInstance
()
:
self
{
{
return
$this
->
addRoute
(
'GET'
,
$uri
,
$controller
,
$method
);
if
(
self
::
$instance
===
null
)
{
self
::
$instance
=
new
self
();
}
return
self
::
$instance
;
}
}
public
function
post
(
string
$uri
,
string
$controller
,
string
$method
)
:
self
/**
* Resolve middleware alias to full class name.
*/
public
static
function
resolveMiddleware
(
string
$middleware
)
:
string
{
{
return
$this
->
addRoute
(
'POST'
,
$uri
,
$controller
,
$method
)
;
return
self
::
$middlewareAliases
[
$middleware
]
??
$middleware
;
}
}
public
function
put
(
string
$uri
,
string
$controller
,
string
$method
)
:
self
// ─── Static Route Registration (supports both calling patterns) ───
/**
* Supports:
* Router::get('/path', [Controller::class, 'method'])
* Router::get('/path', Controller::class, 'method')
*/
public
static
function
get
(
string
$uri
,
$handler
,
?
string
$action
=
null
)
:
self
{
{
return
$this
->
addRoute
(
'PUT'
,
$uri
,
$controller
,
$method
);
return
self
::
getInstance
()
->
addRoute
(
'GET'
,
$uri
,
$handler
,
$action
);
}
}
public
function
delete
(
string
$uri
,
string
$controller
,
string
$method
)
:
self
public
static
function
post
(
string
$uri
,
$handler
,
?
string
$action
=
null
)
:
self
{
{
return
$this
->
addRoute
(
'DELETE'
,
$uri
,
$controller
,
$method
);
return
self
::
getInstance
()
->
addRoute
(
'POST'
,
$uri
,
$handler
,
$action
);
}
}
public
function
group
(
array
$attributes
,
callable
$callback
)
:
void
public
static
function
put
(
string
$uri
,
$handler
,
?
string
$action
=
null
)
:
self
{
{
$this
->
groupStack
[]
=
$attributes
;
return
self
::
getInstance
()
->
addRoute
(
'PUT'
,
$uri
,
$handler
,
$action
);
$callback
(
$this
);
}
array_pop
(
$this
->
groupStack
);
public
static
function
delete
(
string
$uri
,
$handler
,
?
string
$action
=
null
)
:
self
{
return
self
::
getInstance
()
->
addRoute
(
'DELETE'
,
$uri
,
$handler
,
$action
);
}
/**
* Route group. Supports both calling patterns:
*
* Router::group('/prefix', ['middleware' => ['auth']], function() { ... })
* $router->group(['prefix' => '/x', 'middleware' => [Class::class]], function($r) { ... })
*/
public
static
function
group
(
$prefixOrAttributes
,
$attributesOrCallback
=
null
,
$callback
=
null
)
:
void
{
$instance
=
self
::
getInstance
();
if
(
is_string
(
$prefixOrAttributes
))
{
// Pattern: group('/prefix', ['middleware' => [...]], callable)
// OR: group('/prefix', callable)
if
(
is_callable
(
$attributesOrCallback
)
&&
$callback
===
null
)
{
$attributes
=
[
'prefix'
=>
$prefixOrAttributes
];
$cb
=
$attributesOrCallback
;
}
else
{
$attributes
=
is_array
(
$attributesOrCallback
)
?
$attributesOrCallback
:
[];
$attributes
[
'prefix'
]
=
$prefixOrAttributes
;
$cb
=
$callback
;
}
}
elseif
(
is_array
(
$prefixOrAttributes
))
{
// Pattern: group(['prefix' => '/x', 'middleware' => [...]], callable)
$attributes
=
$prefixOrAttributes
;
$cb
=
$attributesOrCallback
;
}
else
{
return
;
}
$instance
->
groupStack
[]
=
$attributes
;
if
(
is_callable
(
$cb
))
{
$cb
(
$instance
);
}
}
public
function
middleware
(
array
|
string
$middleware
)
:
self
array_pop
(
$instance
->
groupStack
);
}
/**
* Chainable middleware on last registered route.
*/
public
function
middleware
(
$middleware
)
:
self
{
{
$last
=
array_key_last
(
$this
->
routes
);
$last
=
array_key_last
(
$this
->
routes
);
if
(
$last
!==
null
)
{
if
(
$last
!==
null
)
{
...
@@ -48,8 +127,23 @@ final class Router
...
@@ -48,8 +127,23 @@ final class Router
return
$this
;
return
$this
;
}
}
private
function
addRoute
(
string
$httpMethod
,
string
$uri
,
string
$controller
,
string
$action
)
:
self
// ─── Internal Route Registration ───
private
function
addRoute
(
string
$httpMethod
,
string
$uri
,
$handler
,
?
string
$action
=
null
)
:
self
{
{
// Handle [Controller::class, 'method'] array syntax
if
(
is_array
(
$handler
))
{
$controller
=
$handler
[
0
];
$action
=
$handler
[
1
]
??
$action
;
}
else
{
$controller
=
$handler
;
}
if
(
!
$action
)
{
throw
new
\RuntimeException
(
"Route
{
$httpMethod
}
{
$uri
}
: no action/method specified."
);
}
// Build prefix and middleware from group stack
$prefix
=
''
;
$prefix
=
''
;
$middleware
=
[];
$middleware
=
[];
...
@@ -63,6 +157,9 @@ final class Router
...
@@ -63,6 +157,9 @@ final class Router
}
}
}
}
// Resolve middleware aliases to full class names
$middleware
=
array_map
([
self
::
class
,
'resolveMiddleware'
],
$middleware
);
$fullUri
=
$prefix
.
'/'
.
ltrim
(
$uri
,
'/'
);
$fullUri
=
$prefix
.
'/'
.
ltrim
(
$uri
,
'/'
);
$fullUri
=
'/'
.
trim
(
$fullUri
,
'/'
);
$fullUri
=
'/'
.
trim
(
$fullUri
,
'/'
);
if
(
$fullUri
!==
'/'
)
{
if
(
$fullUri
!==
'/'
)
{
...
@@ -114,4 +211,9 @@ final class Router
...
@@ -114,4 +211,9 @@ final class Router
return
null
;
return
null
;
}
}
public
function
getRoutes
()
:
array
{
return
$this
->
routes
;
}
}
}
\ No newline at end of file
public/index.php
View file @
2ed51510
...
@@ -3,11 +3,32 @@ declare(strict_types=1);
...
@@ -3,11 +3,32 @@ declare(strict_types=1);
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
// Show errors during development
error_reporting
(
E_ALL
);
ini_set
(
'display_errors'
,
'0'
);
ini_set
(
'log_errors'
,
'1'
);
ini_set
(
'error_log'
,
ROOT_PATH
.
'/storage/logs/php-error.log'
);
require
ROOT_PATH
.
'/bootstrap/autoload.php'
;
require
ROOT_PATH
.
'/bootstrap/autoload.php'
;
require
ROOT_PATH
.
'/bootstrap/app.php'
;
use
Engine\Core\App
;
try
{
use
Engine\Core\Container
;
require
ROOT_PATH
.
'/bootstrap/app.php'
;
$app
=
\Engine\Core\Container
::
getInstance
()
->
resolve
(
\Engine\Core\App
::
class
);
$app
->
run
();
}
catch
(
\Throwable
$e
)
{
error_log
(
"[FATAL]
{
$e
->
getMessage
()
}
in
{
$e
->
getFile
()
}
:
{
$e
->
getLine
()
}
\n
{
$e
->
getTraceAsString
()
}
"
);
$app
=
Container
::
getInstance
()
->
resolve
(
App
::
class
);
http_response_code
(
500
);
$app
->
run
();
if
(
str_contains
(
$_SERVER
[
'HTTP_ACCEPT'
]
??
''
,
'json'
))
{
\ No newline at end of file
header
(
'Content-Type: application/json'
);
echo
json_encode
([
'error'
=>
'Internal Server Error'
,
'message'
=>
$e
->
getMessage
(),
'file'
=>
$e
->
getFile
(),
'line'
=>
$e
->
getLine
()]);
}
else
{
echo
'<!DOCTYPE html><html><head><title>500</title><style>body{font-family:monospace;padding:40px;background:#0f172a;color:#e2e8f0}h1{color:#ef4444}pre{background:#1e293b;padding:20px;border-radius:8px;overflow-x:auto;white-space:pre-wrap}</style></head><body>'
;
echo
'<h1>500 — Server Error</h1>'
;
echo
'<pre>'
.
htmlspecialchars
(
$e
->
getMessage
())
.
"
\n\n
"
;
echo
htmlspecialchars
(
$e
->
getFile
())
.
':'
.
$e
->
getLine
()
.
"
\n\n
"
;
echo
htmlspecialchars
(
$e
->
getTraceAsString
());
echo
'</pre></body></html>'
;
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment