Update: 2026-05-03 17:32:57
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use App\Core\{Request, Response, Router, Container};
|
||||
|
||||
final class Application
|
||||
{
|
||||
private Container $container;
|
||||
private Router $router;
|
||||
public static ?array $config = null;
|
||||
|
||||
public function __construct(string $basePath)
|
||||
{
|
||||
// 1. Load Environment Variables
|
||||
// In local dev, .env is in the project root. In production, it might be moved.
|
||||
$dotenv = Dotenv::createImmutable($basePath);
|
||||
$dotenv->load();
|
||||
|
||||
// 2. Set Timezone
|
||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Asia/Amman');
|
||||
|
||||
// 3. Initialize Core Components
|
||||
$this->container = new Container();
|
||||
|
||||
// 4. Load Configurations
|
||||
$this->loadConfigs($basePath);
|
||||
|
||||
$this->router = new Router($this->container);
|
||||
|
||||
// Register core services in container
|
||||
$this->container->set(Container::class, $this->container);
|
||||
$this->container->set(Router::class, $this->router);
|
||||
}
|
||||
|
||||
private function loadConfigs(string $basePath): void
|
||||
{
|
||||
$configPath = $basePath . '/config';
|
||||
$configs = [];
|
||||
|
||||
foreach (glob($configPath . '/*.php') as $file) {
|
||||
$key = basename($file, '.php');
|
||||
$configs[$key] = require $file;
|
||||
}
|
||||
|
||||
self::$config = $configs;
|
||||
$this->container->set('config', $configs);
|
||||
}
|
||||
|
||||
public function getRouter(): Router
|
||||
{
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// 1. Security Headers
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
|
||||
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' cdn.tailwindcss.com unpkg.com; style-src \'self\' \'unsafe-inline\' fonts.googleapis.com; font-src fonts.gstatic.com');
|
||||
header_remove('X-Powered-By');
|
||||
|
||||
try {
|
||||
$request = new Request();
|
||||
$this->router->dispatch($request, $this->container);
|
||||
} catch (\Throwable $e) {
|
||||
// Global Exception Handler
|
||||
Response::error(
|
||||
'حدث خطأ غير متوقع في النظام',
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
500,
|
||||
[
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
|
||||
final class Container
|
||||
{
|
||||
private array $instances = [];
|
||||
|
||||
public function set(string $id, mixed $concrete): void
|
||||
{
|
||||
$this->instances[$id] = $concrete;
|
||||
}
|
||||
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (isset($this->instances[$id])) {
|
||||
if ($this->instances[$id] instanceof \Closure) {
|
||||
$this->instances[$id] = ($this->instances[$id])($this);
|
||||
}
|
||||
return $this->instances[$id];
|
||||
}
|
||||
|
||||
return $this->resolve($id);
|
||||
}
|
||||
|
||||
public function resolve(string $id): mixed
|
||||
{
|
||||
if (!class_exists($id)) {
|
||||
throw new Exception("Class {$id} cannot be resolved.");
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($id);
|
||||
|
||||
if (!$reflection->isInstantiable()) {
|
||||
throw new Exception("Class {$id} is not instantiable.");
|
||||
}
|
||||
|
||||
$constructor = $reflection->getConstructor();
|
||||
|
||||
if (is_null($constructor)) {
|
||||
return new $id();
|
||||
}
|
||||
|
||||
$parameters = $constructor->getParameters();
|
||||
$dependencies = [];
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
$dependencies[] = $parameter->getDefaultValue();
|
||||
continue;
|
||||
}
|
||||
throw new Exception("Unable to resolve parameter '{$parameter->getName()}' in class {$id}");
|
||||
}
|
||||
|
||||
$dependencies[] = $this->get($type->getName());
|
||||
}
|
||||
|
||||
$instance = $reflection->newInstanceArgs($dependencies);
|
||||
$this->instances[$id] = $instance;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple PDO Database Wrapper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -6,7 +9,6 @@ namespace App\Core;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Exception;
|
||||
|
||||
final class Database
|
||||
{
|
||||
@@ -15,24 +17,27 @@ final class Database
|
||||
public static function getInstance(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$host = $_ENV['DB_HOST'];
|
||||
$db = $_ENV['DB_DATABASE'];
|
||||
$user = $_ENV['DB_USERNAME'];
|
||||
$pass = $_ENV['DB_PASSWORD'];
|
||||
$port = $_ENV['DB_PORT'];
|
||||
$charset = $_ENV['DB_CHARSET'] ?? 'utf8mb4';
|
||||
|
||||
$dsn = "mysql:host=$host;dbname=$db;port=$port;charset=$charset";
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
$config = require APP_PATH . '/config/database.php';
|
||||
|
||||
$dsn = sprintf(
|
||||
"mysql:host=%s;port=%s;dbname=%s;charset=%s",
|
||||
$config['host'],
|
||||
$config['port'],
|
||||
$config['database'],
|
||||
$config['charset']
|
||||
);
|
||||
|
||||
try {
|
||||
self::$instance = new PDO($dsn, $user, $pass, $options);
|
||||
self::$instance = new PDO($dsn, $config['username'], $config['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception("Database Connection Error: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Database connection failed']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
app/Core/JWT.php
Normal file
52
app/Core/JWT.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple JWT (HMAC SHA256)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class JWT
|
||||
{
|
||||
private static function base64UrlEncode(string $data): string
|
||||
{
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
private static function base64UrlDecode(string $data): string
|
||||
{
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
|
||||
public static function encode(array $payload, string $secret): string
|
||||
{
|
||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||
$base64UrlHeader = self::base64UrlEncode($header);
|
||||
$base64UrlPayload = self::base64UrlEncode(json_encode($payload));
|
||||
|
||||
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
$base64UrlSignature = self::base64UrlEncode($signature);
|
||||
|
||||
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||
}
|
||||
|
||||
public static function decode(string $token, string $secret): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) return null;
|
||||
|
||||
[$header, $payload, $signature] = $parts;
|
||||
|
||||
$expectedSignature = self::base64UrlEncode(hash_hmac('sha256', $header . "." . $payload, $secret, true));
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) return null;
|
||||
|
||||
$decodedPayload = json_decode(self::base64UrlDecode($payload), true);
|
||||
|
||||
// Check expiry
|
||||
if (isset($decodedPayload['exp']) && $decodedPayload['exp'] < time()) return null;
|
||||
|
||||
return $decodedPayload;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Predis\Client;
|
||||
use Exception;
|
||||
|
||||
final class Redis
|
||||
{
|
||||
private static ?Client $instance = null;
|
||||
|
||||
public static function getInstance(): Client
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
try {
|
||||
self::$instance = new Client([
|
||||
'scheme' => 'tcp',
|
||||
'host' => $_ENV['REDIS_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['REDIS_PORT'] ?? 6379,
|
||||
'password' => $_ENV['REDIS_PASSWORD'] ?: null,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// If Redis fails, we might want to log it or handle gracefully
|
||||
// depending on how critical it is.
|
||||
throw new Exception("Redis Connection Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Request
|
||||
{
|
||||
private string $method;
|
||||
private string $path;
|
||||
private array $headers;
|
||||
private array $queryParams;
|
||||
private array $body;
|
||||
private array $files;
|
||||
public ?object $user = null; // Populated by AuthMiddleware
|
||||
public ?string $tenantId = null; // Populated by TenantMiddleware
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Read API path from query string: index.php?route=/api/v1/auth/login
|
||||
$this->path = $_GET['route'] ?? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$this->headers = getallheaders();
|
||||
$this->queryParams = $_GET;
|
||||
$this->files = $_FILES;
|
||||
|
||||
$contentType = $this->getHeader('Content-Type') ?? $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if ($contentType && str_contains(strtolower($contentType), 'application/json')) {
|
||||
$this->body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
} else {
|
||||
$this->body = $_POST;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMethod(): string { return $this->method; }
|
||||
public function getPath(): string { return $this->path; }
|
||||
public function getHeaders(): array { return $this->headers; }
|
||||
public function getQueryParams(): array { return $this->queryParams; }
|
||||
public function getBody(): array { return $this->body; }
|
||||
public function getFiles(): array { return $this->files; }
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
$name = strtolower($name);
|
||||
foreach ($this->headers as $key => $value) {
|
||||
if (strtolower($key) === $name) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function input(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->body[$key] ?? $this->queryParams[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Response
|
||||
{
|
||||
public static function json(array $data, int $status = 200, array $headers = []): void
|
||||
{
|
||||
self::send($data, $status, array_merge(['Content-Type' => 'application/json; charset=utf-8'], $headers));
|
||||
}
|
||||
|
||||
public static function error(string $messageAr, string $code, int $status = 400, ?array $details = null): void
|
||||
{
|
||||
$data = [
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'message_ar' => $messageAr,
|
||||
'code' => $code,
|
||||
'details' => $details
|
||||
]
|
||||
];
|
||||
self::json($data, $status);
|
||||
}
|
||||
|
||||
private static function send(mixed $data, int $status, array $headers): void
|
||||
{
|
||||
http_response_code($status);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
header("$name: $value");
|
||||
}
|
||||
|
||||
// Apply Security Headers
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header_remove('X-Powered-By');
|
||||
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use FastRoute\RouteCollector;
|
||||
use function FastRoute\simpleDispatcher;
|
||||
|
||||
final class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
public Container $container;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function addRoute(string $method, string $path, array|callable $handler): void
|
||||
{
|
||||
$this->routes[] = [$method, $path, $handler];
|
||||
}
|
||||
|
||||
public function dispatch(Request $request): void
|
||||
{
|
||||
$dispatcher = simpleDispatcher(function (RouteCollector $r) {
|
||||
foreach ($this->routes as $route) {
|
||||
$r->addRoute($route[0], $route[1], $route[2]);
|
||||
}
|
||||
});
|
||||
|
||||
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getPath());
|
||||
|
||||
switch ($routeInfo[0]) {
|
||||
case \FastRoute\Dispatcher::NOT_FOUND:
|
||||
Response::error('المسار غير موجود', 'NOT_FOUND', 404);
|
||||
break;
|
||||
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
|
||||
Response::error('الطريقة غير مسموح بها', 'METHOD_NOT_ALLOWED', 405);
|
||||
break;
|
||||
case \FastRoute\Dispatcher::FOUND:
|
||||
$handler = $routeInfo[1];
|
||||
$vars = $routeInfo[2];
|
||||
|
||||
$this->executeHandler($handler, $request, $this->container, $vars);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function executeHandler(mixed $handler, Request $request, Container $container, array $vars): void
|
||||
{
|
||||
if (is_array($handler) && isset($handler['middleware'])) {
|
||||
$middlewares = (array) $handler['middleware'];
|
||||
$finalHandler = $handler['handler'];
|
||||
|
||||
$pipeline = $this->createPipeline($middlewares, $finalHandler, $container, $vars);
|
||||
$pipeline($request);
|
||||
} else {
|
||||
$this->callHandler($handler, $request, $container, $vars);
|
||||
}
|
||||
}
|
||||
|
||||
private function createPipeline(array $middlewares, mixed $handler, Container $container, array $vars): callable
|
||||
{
|
||||
return array_reduce(
|
||||
array_reverse($middlewares),
|
||||
function ($next, $middleware) use ($container) {
|
||||
return function ($request) use ($next, $middleware, $container) {
|
||||
$parts = explode(':', $middleware);
|
||||
$className = $parts[0];
|
||||
$args = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
||||
|
||||
$instance = $container->get($className);
|
||||
return $instance->handle($request, $next, ...$args);
|
||||
};
|
||||
},
|
||||
function ($request) use ($handler, $container, $vars) {
|
||||
$this->callHandler($handler, $request, $container, $vars);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function callHandler(mixed $handler, Request $request, Container $container, array $vars): void
|
||||
{
|
||||
if (is_array($handler)) {
|
||||
[$controllerClass, $method] = $handler;
|
||||
$controller = $container->get($controllerClass);
|
||||
$controller->$method($request, ...array_values($vars));
|
||||
} else {
|
||||
$handler($request, ...array_values($vars));
|
||||
}
|
||||
}
|
||||
}
|
||||
21
app/Core/Security.php
Normal file
21
app/Core/Security.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Security Helpers
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Security
|
||||
{
|
||||
public static function sanitize(string $data): string
|
||||
{
|
||||
return htmlspecialchars(strip_tags(trim($data)));
|
||||
}
|
||||
|
||||
public static function generateRandomString(int $length = 32): string
|
||||
{
|
||||
return bin2hex(random_bytes($length / 2));
|
||||
}
|
||||
}
|
||||
25
app/Core/Validator.php
Normal file
25
app/Core/Validator.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Data Validator
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Validator
|
||||
{
|
||||
public static function validate(array $data, array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (str_contains($rule, 'required') && (empty($data[$field]) && $data[$field] !== '0')) {
|
||||
$errors[$field] = "The {$field} field is required.";
|
||||
}
|
||||
if (str_contains($rule, 'email') && !empty($data[$field]) && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field] = "The {$field} must be a valid email address.";
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!function_exists('config')) {
|
||||
/**
|
||||
* Get a configuration value using dot notation.
|
||||
* Example: config('app.name')
|
||||
*/
|
||||
function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$configs = \App\Core\Application::$config;
|
||||
|
||||
if ($configs === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$parts = explode('.', $key);
|
||||
$value = $configs;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (!isset($value[$part])) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$part];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('env')) {
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_ENV[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user