Compare commits
2 commits
0111ef9525
...
d0760ed95c
Author | SHA1 | Date | |
---|---|---|---|
|
d0760ed95c | ||
|
e8d8e0e95b |
14 changed files with 461 additions and 116 deletions
22
app/Controllers/TestController.php
Normal file
22
app/Controllers/TestController.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controllers\Controller;
|
||||
use Core\View\Render;
|
||||
|
||||
class TestController extends Controller
|
||||
{
|
||||
/**
|
||||
* Render index
|
||||
*
|
||||
* @return \Core\View\Render
|
||||
*/
|
||||
public function test(int $id): Render
|
||||
{
|
||||
|
||||
dd($id);
|
||||
|
||||
return $this->response->view('subnet');
|
||||
}
|
||||
}
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
use App\Controllers\Api\SubnetController;
|
||||
use App\Controllers\HomeController;
|
||||
use Core\Router\Router;
|
||||
use App\Controllers\TestController;
|
||||
use Core\Routing\Route;
|
||||
|
||||
Router::get('/', HomeController::class, 'index');
|
||||
Router::post('/api/subnet', SubnetController::class, 'data');
|
||||
Route::get('/', HomeController::class, 'index');
|
||||
Route::post('/api/subnet', SubnetController::class, 'data');
|
||||
Route::get('/test/{id}', TestController::class, 'test');
|
||||
Route::get('/test/{id}/update', TestController::class, 'test');
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Core\Env\Env;
|
||||
use Core\Router\Router;
|
||||
use Core\Routing\Router;
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
|
|
|
@ -77,5 +77,7 @@ class Exceptions
|
|||
public static function catchOne(Throwable $exception): void
|
||||
{
|
||||
self::instance()->handleException($exception);
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
5
src/Exceptions/Exceptions/HttpException.php
Normal file
5
src/Exceptions/Exceptions/HttpException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Exceptions\Exceptions;
|
||||
|
||||
class HttpException extends \Exception {}
|
5
src/Exceptions/Exceptions/InvalidArgumentException.php
Normal file
5
src/Exceptions/Exceptions/InvalidArgumentException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Exceptions\Exceptions;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException {}
|
5
src/Exceptions/Exceptions/NotFoundHttpException.php
Normal file
5
src/Exceptions/Exceptions/NotFoundHttpException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Exceptions\Exceptions;
|
||||
|
||||
class NotFoundHttpException extends HttpException {}
|
|
@ -2,8 +2,25 @@
|
|||
|
||||
namespace Core\Http;
|
||||
|
||||
use Core\Exceptions\Exceptions;
|
||||
|
||||
class Request
|
||||
{
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* Build request object
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
// Capture all exceptions
|
||||
Exceptions::catch($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request method
|
||||
*
|
||||
|
@ -43,7 +60,7 @@ class Request
|
|||
*/
|
||||
public final function has(string $param): bool
|
||||
{
|
||||
return isset($_POST[$param]);
|
||||
return isset($this->data[$param]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,11 +73,11 @@ class Request
|
|||
public final function get(string|null $param = null, mixed $default = null): mixed
|
||||
{
|
||||
if($param == null) {
|
||||
return $_POST;
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
if($this->has($param)) {
|
||||
return $_POST[$param];
|
||||
return $this->data[$param];
|
||||
}
|
||||
|
||||
return $default;
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Router;
|
||||
|
||||
use Core\Exceptions\Exceptions;
|
||||
use Core\Http\Request;
|
||||
use Core\View\Render;
|
||||
use Exception;
|
||||
|
||||
class Router
|
||||
{
|
||||
/**
|
||||
* List of routes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $routes = [];
|
||||
|
||||
/**
|
||||
* Add GET route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function get(string $route, string $controller, string $action): void
|
||||
{
|
||||
self::register($route, $controller, $action, "GET");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add POST route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function post(string $route, string $controller, string $action): void
|
||||
{
|
||||
self::register($route, $controller, $action, "POST");
|
||||
}
|
||||
|
||||
/**
|
||||
* Register route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
public static function register(string $route, string $controller, string $action, string $method): void
|
||||
{
|
||||
self::$routes[$method][$route] = [
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch router and run application
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function dispatch(): void
|
||||
{
|
||||
// Init request
|
||||
$request = new Request();
|
||||
$url = $request->url();
|
||||
$method = $request->method();
|
||||
|
||||
// Capture all exceptions
|
||||
Exceptions::catch($request);
|
||||
|
||||
try {
|
||||
if (array_key_exists($url, self::$routes[$method])) {
|
||||
$controllerName = self::$routes[$method][$url]['controller'];
|
||||
$actionName = self::$routes[$method][$url]['action'];
|
||||
|
||||
// Check for controller existence
|
||||
if (!class_exists($controllerName)) {
|
||||
throw new Exception(sprintf("Controller '%s' missing", $controllerName));
|
||||
}
|
||||
|
||||
// Create controller object
|
||||
$controller = new $controllerName($request);
|
||||
|
||||
// Check for method on controller
|
||||
if (!method_exists($controller, $actionName)) {
|
||||
throw new Exception(sprintf("Method '%s' not found on '%s'", $actionName, $controllerName));
|
||||
}
|
||||
|
||||
// Build response object of action
|
||||
$response = $controller->$actionName();
|
||||
|
||||
// Render action
|
||||
if ($response instanceof Render) {
|
||||
$response->render();
|
||||
}
|
||||
} else {
|
||||
throw new Exception(sprintf("No route found for: %s", $url));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Exceptions::catchOne($e);
|
||||
}
|
||||
}
|
||||
}
|
32
src/Routing/Route.php
Normal file
32
src/Routing/Route.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Routing;
|
||||
|
||||
class Route
|
||||
{
|
||||
/**
|
||||
* Add GET route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function get(string $route, string $controller, string $action): void
|
||||
{
|
||||
RouteCollection::register($route, $controller, $action, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add POST route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function post(string $route, string $controller, string $action): void
|
||||
{
|
||||
RouteCollection::register($route, $controller, $action, 'POST');
|
||||
}
|
||||
}
|
90
src/Routing/RouteCollection.php
Normal file
90
src/Routing/RouteCollection.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Routing;
|
||||
|
||||
class RouteCollection
|
||||
{
|
||||
/**
|
||||
* A majestic vault of all defined routes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $routes = [];
|
||||
|
||||
/**
|
||||
* Add a route to the sacred collection.
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
public static function register(string $route, string $controller, string $action, string $method): void
|
||||
{
|
||||
// Transform human-readable route syntax into regex.
|
||||
$routeRegex = self::convertToRegex($route);
|
||||
|
||||
// Store the route in the sacred collection
|
||||
self::$routes[$method][$routeRegex] = [
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
'original' => $route,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal the treasure trove of routes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function retrieve(): array
|
||||
{
|
||||
return self::$routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a route pattern into a regex for matching requests.
|
||||
*
|
||||
* @param string $route
|
||||
* @return string
|
||||
*/
|
||||
protected static function convertToRegex(string $route): string
|
||||
{
|
||||
$regex = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<\1>[a-zA-Z0-9_-]+)', $route);
|
||||
return '#^' . $regex . '$#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific route by method and URL.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
public static function find(string $method, string $url): ?array
|
||||
{
|
||||
foreach (self::$routes[$method] ?? [] as $routeRegex => $route) {
|
||||
if (preg_match($routeRegex, $url, $matches)) {
|
||||
$route['params'] = array_filter(
|
||||
$matches,
|
||||
fn($key) => !is_numeric($key),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all routes from the collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear(): void
|
||||
{
|
||||
self::$routes = [];
|
||||
}
|
||||
}
|
162
src/Routing/RouteDispatcher.php
Normal file
162
src/Routing/RouteDispatcher.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Routing;
|
||||
|
||||
use Core\Env\Env;
|
||||
use Core\Exceptions\Exceptions;
|
||||
use Core\Exceptions\Exceptions\NotFoundHttpException;
|
||||
use Core\Http\Request;
|
||||
use Core\View\Render;
|
||||
use Exception;
|
||||
|
||||
class RouteDispatcher
|
||||
{
|
||||
/**
|
||||
* Current request instance
|
||||
*
|
||||
* @var \Core\Http\Request
|
||||
*/
|
||||
private Request $request;
|
||||
|
||||
/**
|
||||
* Collection of all routes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $routeCollection;
|
||||
|
||||
/**
|
||||
* @param \Core\Http\Request $request
|
||||
* @param array $routeCollection
|
||||
*/
|
||||
public function __construct(Request $request, array $routeCollection)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->routeCollection = $routeCollection;
|
||||
|
||||
try {
|
||||
$route = $this->findMatchingRoute();
|
||||
$controller = $this->instantiateController($route['controller']);
|
||||
$action = $this->validateActionExists($controller, $route['action']);
|
||||
$params = $this->resolveParameters($controller, $action, $route['params'] ?? []);
|
||||
|
||||
$this->executeAction($controller, $action, $params);
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$this->handleException($e, 404, 'page not found');
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e, 500, 'something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
public static function dispatch(Request $request, array $routeCollection): void
|
||||
{
|
||||
new self($request, $routeCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate a matching route for the incoming request.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Core\Exceptions\Exceptions\NotFoundHttpException
|
||||
*/
|
||||
private function findMatchingRoute(): array
|
||||
{
|
||||
$url = $this->request->url();
|
||||
$method = $this->request->method();
|
||||
|
||||
foreach ($this->routeCollection[$method] ?? [] as $routeRegex => $route) {
|
||||
if (preg_match($routeRegex, $url, $matches)) {
|
||||
$params = array_filter(
|
||||
$matches,
|
||||
fn($key) => !is_numeric($key),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
return array_merge($route, ['params' => $params]);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException(sprintf("No route found for: %s", $url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the requested controller.
|
||||
*
|
||||
* @param string $controllerName
|
||||
* @return object
|
||||
* @throws \Core\Exceptions\Exceptions\NotFoundHttpException
|
||||
*/
|
||||
private function instantiateController(string $controllerName): object
|
||||
{
|
||||
if (!class_exists($controllerName)) {
|
||||
throw new NotFoundHttpException(sprintf("Controller '%s' missing", $controllerName));
|
||||
}
|
||||
|
||||
return new $controllerName($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the action exists in the controller.
|
||||
*
|
||||
* @param object $controller
|
||||
* @param string $actionName
|
||||
* @return string
|
||||
* @throws \Core\Exceptions\Exceptions\NotFoundHttpException
|
||||
*/
|
||||
private function validateActionExists(object $controller, string $actionName): string
|
||||
{
|
||||
if (!method_exists($controller, $actionName)) {
|
||||
throw new NotFoundHttpException(sprintf("Method '%s' not found on '%s'", $actionName, get_class($controller)));
|
||||
}
|
||||
|
||||
return $actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and resolve parameters for the controller action.
|
||||
*
|
||||
* @param object $controller
|
||||
* @param string $action
|
||||
* @param array $params
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function resolveParameters(object $controller, string $action, array $params): array
|
||||
{
|
||||
return RouteValidator::resolve($controller, $action, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the resolved action on the controller with validated parameters.
|
||||
*
|
||||
* @param object $controller
|
||||
* @param string $action
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
private function executeAction(object $controller, string $action, array $params): void
|
||||
{
|
||||
$response = $controller->$action(...$params);
|
||||
|
||||
if ($response instanceof Render) {
|
||||
$response->render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle exceptions gracefully.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param int $statusCode
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function handleException(Exception $e, int $statusCode, string $message): void
|
||||
{
|
||||
if (Env::get('debug')) {
|
||||
Exceptions::catchOne($e);
|
||||
}
|
||||
|
||||
http_response_code($statusCode);
|
||||
echo $message;
|
||||
}
|
||||
}
|
89
src/Routing/RouteValidator.php
Normal file
89
src/Routing/RouteValidator.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Routing;
|
||||
|
||||
use Core\Exceptions\Exceptions\NotFoundHttpException;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
|
||||
class RouteValidator
|
||||
{
|
||||
/**
|
||||
* Validate parameters against the method signature.
|
||||
*
|
||||
* @param object $controller
|
||||
* @param string $action
|
||||
* @param array $params
|
||||
* @return array
|
||||
* @throws \Core\Exceptions\Exceptions\NotFoundHttpException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function resolve(object $controller, string $action, array $params): array
|
||||
{
|
||||
$reflection = new ReflectionMethod($controller, $action);
|
||||
$methodParameters = $reflection->getParameters();
|
||||
$validatedParams = [];
|
||||
|
||||
foreach ($methodParameters as $methodParameter) {
|
||||
$paramName = $methodParameter->getName();
|
||||
$paramType = $methodParameter->getType();
|
||||
$isOptional = $methodParameter->isOptional();
|
||||
$defaultValue = $isOptional ? $methodParameter->getDefaultValue() : null;
|
||||
|
||||
if (!array_key_exists($paramName, $params)) {
|
||||
if ($isOptional) {
|
||||
$validatedParams[$paramName] = $defaultValue;
|
||||
continue;
|
||||
}
|
||||
throw new NotFoundHttpException(sprintf("Missing parameter '%s' for action '%s'", $paramName, $action));
|
||||
}
|
||||
|
||||
$value = $params[$paramName];
|
||||
$validatedParams[$paramName] = self::validateType($paramName, $value, $paramType, $action);
|
||||
}
|
||||
|
||||
return $validatedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and cast a parameter based on its type.
|
||||
*
|
||||
* @param string $paramName
|
||||
* @param mixed $value
|
||||
* @param ReflectionNamedType|null $paramType
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
* @throws \Core\Exceptions\Exceptions\NotFoundHttpException
|
||||
*/
|
||||
private static function validateType(string $paramName, mixed $value, ?ReflectionNamedType $paramType, string $action): mixed
|
||||
{
|
||||
if (!$paramType || $paramType->allowsNull() && $value === null) {
|
||||
return $value; // No type or allows null, return as-is.
|
||||
}
|
||||
|
||||
$typeName = $paramType->getName();
|
||||
|
||||
switch ($typeName) {
|
||||
case 'int':
|
||||
if (!ctype_digit((string)$value)) {
|
||||
throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected integer, got '%s'", $paramName, gettype($value)));
|
||||
}
|
||||
return (int)$value;
|
||||
|
||||
case 'float':
|
||||
if (!is_numeric($value)) {
|
||||
throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected float, got '%s'", $paramName, gettype($value)));
|
||||
}
|
||||
return (float)$value;
|
||||
|
||||
case 'string':
|
||||
if (!is_string($value)) {
|
||||
throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected string, got '%s'", $paramName, gettype($value)));
|
||||
}
|
||||
return $value;
|
||||
|
||||
default:
|
||||
throw new NotFoundHttpException(sprintf("Unsupported type '%s' for parameter '%s'", $typeName, $paramName));
|
||||
}
|
||||
}
|
||||
}
|
22
src/Routing/Router.php
Normal file
22
src/Routing/Router.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Routing;
|
||||
|
||||
use Core\Http\Request;
|
||||
|
||||
class Router
|
||||
{
|
||||
/**
|
||||
* Dispatch router and run application
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function dispatch(): void
|
||||
{
|
||||
// Create request
|
||||
$request = new Request($_POST + $_FILES);
|
||||
|
||||
// Dispatch router
|
||||
RouteDispatcher::dispatch($request, RouteCollection::retrieve());
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue