parent
1c633b9ef6
commit
809a95f4eb
@ -0,0 +1,72 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace CodeHydrater\apis\rate_limiting; |
||||
|
||||
use CodeHydrater\enums\api_tier as API_TIER; |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
|
||||
class api_authenticator { |
||||
private $validation_callback; |
||||
private $rate_limiter; |
||||
|
||||
public function __construct(API_TIER $tier, callable $validation_callback, $rate_limiter = null) { |
||||
$this->validation_callback = $validation_callback; |
||||
$this->rate_limiter = $rate_limiter; |
||||
} |
||||
|
||||
function authenticate(string $provided_key): false|array { |
||||
if (empty($provided_key)) { |
||||
$headers = getallheaders(); |
||||
$provided_key = $headers['X-API-KEY'] ?? $_GET['api_key'] ?? null; |
||||
} |
||||
if (strlen($provided_key) < 5) { |
||||
throw new Exception('API key required', 401); |
||||
} |
||||
|
||||
// Rate limiting |
||||
if ($this->rate_limiter) { |
||||
$rate_limit_result = $this->rate_limiter->check_rate_limit($provided_key); |
||||
|
||||
if (!$rate_limit_result['allowed']) { |
||||
header('X-RateLimit-Limit: ' . $rate_limiter->get_limit()); |
||||
header('X-RateLimit-Remaining: ' . $rate_limit_result['remaining']); |
||||
header('X-RateLimit-Reset: ' . $rate_limit_result['reset']); |
||||
header('X-RateLimit-Burst-Limit: ' . $rate_limiter->get_burst_limit()); |
||||
header('X-RateLimit-Burst-Remaining: ' . $rate_limit_result['burst_remaining']); |
||||
header('X-RateLimit-Burst-Reset: ' . $rate_limit_result['burst_reset']); |
||||
|
||||
$message = $rate_limit_result['burst_remaining'] === 0 ? 'Burst rate limit exceeded' : 'Rate limit exceeded'; |
||||
|
||||
http_response_code(429); |
||||
die(json_encode([ |
||||
'error' => $message, |
||||
'retry_after' => $rate_limit_result['burst_remaining'] === 0 ? $rate_limit_result['burst_reset'] - time() : $rate_limit_result['reset'] - time() |
||||
])); |
||||
} |
||||
|
||||
// Add headers to successful requests |
||||
header('X-RateLimit-Limit: ' . $rate_limiter->get_limit()); |
||||
header('X-RateLimit-Remaining: ' . $rate_limit_result['remaining']); |
||||
header('X-RateLimit-Reset: ' . $rate_limit_result['reset']); |
||||
header('X-RateLimit-Burst-Limit: ' . $rate_limiter->get_burst_limit()); |
||||
header('X-RateLimit-Burst-Remaining: ' . $rate_limit_result['burst_remaining']); |
||||
header('X-RateLimit-Burst-Reset: ' . $rate_limit_result['burst_reset']); |
||||
} |
||||
|
||||
// Execute validation callback |
||||
$customer_data = call_user_func($this->validation_callback, $provided_key); |
||||
|
||||
if (!$customer_data) { |
||||
throw new Exception('Invalid API key', 403); |
||||
} |
||||
|
||||
return $customer_data; |
||||
} |
||||
} |
||||
@ -0,0 +1,151 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\apis\rate_limiting; |
||||
|
||||
class create_subscription_aware_validator { |
||||
|
||||
public function validator(PDO $db): callable |
||||
{ |
||||
return function(string $api_key) use ($db): array { |
||||
$stmt = $db->prepare(" |
||||
SELECT |
||||
k.customer_id, |
||||
k.tier AS key_tier, |
||||
k.is_active, |
||||
k.expires_at, |
||||
s.plan_id, |
||||
p.name AS tier_name, |
||||
p.rate_limit, |
||||
p.burst_limit, |
||||
p.features |
||||
FROM customer_api_keys k |
||||
JOIN customers c ON k.customer_id = c.id |
||||
LEFT JOIN customer_subscriptions s ON |
||||
s.customer_id = c.id AND |
||||
s.is_active = TRUE AND |
||||
(s.ends_at IS NULL OR s.ends_at > NOW()) |
||||
LEFT JOIN subscription_plans p ON s.plan_id = p.id |
||||
WHERE k.api_key = ? |
||||
"); |
||||
$stmt->execute([$api_key]); |
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC); |
||||
|
||||
if (!$data) { |
||||
throw new Exception('Invalid API key', 403); |
||||
} |
||||
|
||||
// Determine the effective tier (key override or subscription) |
||||
$data['effective_tier'] = $data['key_tier'] ?? $data['tier_name'] ?? 'free'; |
||||
|
||||
// Standard validations |
||||
if (!$data['is_active']) { |
||||
throw new Exception('API key disabled', 403); |
||||
} |
||||
|
||||
if ($data['expires_at'] && strtotime($data['expires_at']) < time()) { |
||||
throw new Exception('API key expired', 403); |
||||
} |
||||
|
||||
return $data; |
||||
}; |
||||
} |
||||
|
||||
public function get_create_tables_sql(): string { |
||||
return "-- Customers table |
||||
CREATE TABLE customers ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
name VARCHAR(100) NOT NULL, |
||||
email VARCHAR(100) NOT NULL UNIQUE, |
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
||||
); |
||||
|
||||
-- Subscription plans table |
||||
CREATE TABLE subscription_plans ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
name VARCHAR(50) NOT NULL UNIQUE, -- 'free', 'pro', 'enterprise' |
||||
monthly_price DECIMAL(10,2) NOT NULL, |
||||
rate_limit INT NOT NULL, -- requests per hour |
||||
burst_limit INT NOT NULL, -- requests per second |
||||
features JSON NOT NULL -- additional features |
||||
); |
||||
|
||||
-- Customer subscriptions table |
||||
CREATE TABLE customer_subscriptions ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
customer_id INT NOT NULL, |
||||
plan_id INT NOT NULL, |
||||
starts_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
ends_at TIMESTAMP NULL, -- NULL for ongoing subscriptions |
||||
is_active BOOLEAN DEFAULT TRUE, |
||||
FOREIGN KEY (customer_id) REFERENCES customers(id), |
||||
FOREIGN KEY (plan_id) REFERENCES subscription_plans(id) |
||||
); |
||||
|
||||
CREATE TABLE customer_api_keys ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
customer_id INT NOT NULL, |
||||
api_key VARCHAR(64) NOT NULL, |
||||
secret_key VARCHAR(64) NULL COMMENT 'Optional second factor for HMAC authentication', |
||||
key_name VARCHAR(100) NULL COMMENT 'Descriptive name for the key', |
||||
tier VARCHAR(20) NULL COMMENT 'Overrides customer subscription tier if set', |
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE, |
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
last_used_at TIMESTAMP NULL, |
||||
expires_at TIMESTAMP NULL COMMENT 'Optional expiration date', |
||||
revoked_at TIMESTAMP NULL, |
||||
ip_restrictions TEXT NULL COMMENT 'JSON array of allowed IPs/CIDR ranges', |
||||
referer_restrictions TEXT NULL COMMENT 'JSON array of allowed HTTP referers', |
||||
permissions TEXT NULL COMMENT 'JSON array of permissions/scopes', |
||||
rate_limit_override INT NULL COMMENT 'Custom rate limit (requests per hour)', |
||||
burst_limit_override INT NULL COMMENT 'Custom burst limit (requests per second)', |
||||
metadata JSON NULL COMMENT 'Additional key metadata', |
||||
|
||||
UNIQUE INDEX idx_api_key (api_key), |
||||
INDEX idx_customer_id (customer_id), |
||||
INDEX idx_is_active (is_active), |
||||
INDEX idx_expires_at (expires_at), |
||||
|
||||
FOREIGN KEY (customer_id) |
||||
REFERENCES customers(id) |
||||
ON DELETE CASCADE |
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||
|
||||
CREATE TABLE api_key_permissions ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
permission_key VARCHAR(50) NOT NULL, |
||||
description TEXT NULL, |
||||
UNIQUE INDEX idx_permission_key (permission_key) |
||||
); |
||||
|
||||
CREATE TABLE api_key_permission_mapping ( |
||||
api_key_id INT NOT NULL, |
||||
permission_id INT NOT NULL, |
||||
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
granted_by INT NULL COMMENT 'User who granted this permission', |
||||
|
||||
PRIMARY KEY (api_key_id, permission_id), |
||||
FOREIGN KEY (api_key_id) REFERENCES customer_api_keys(id) ON DELETE CASCADE, |
||||
FOREIGN KEY (permission_id) REFERENCES api_key_permissions(id) ON DELETE CASCADE |
||||
); |
||||
|
||||
CREATE TABLE api_rate_limits ( |
||||
id INT AUTO_INCREMENT PRIMARY KEY, |
||||
api_key VARCHAR(64) NOT NULL, |
||||
request_count INT NOT NULL DEFAULT 0, |
||||
burst_tokens INT NOT NULL DEFAULT 10, |
||||
burst_last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
last_request TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
FOREIGN KEY (api_key) REFERENCES customer_api_keys(api_key), |
||||
INDEX (api_key) |
||||
); |
||||
"; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,155 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\apis\rate_limiting; |
||||
|
||||
class rate_limiter { |
||||
private $db; |
||||
private $limit; // Requests per window (long-term) |
||||
private $window; // Window in seconds (long-term) |
||||
private $burst_limit; // Maximum burst capacity |
||||
private $burst_refill; // How many tokens refill per second |
||||
|
||||
public function __construct( |
||||
PDO $db, |
||||
int $limit = 100, |
||||
int $window = 3600, |
||||
int $burst_limit = 5, |
||||
int $burst_refill = 1 |
||||
) { |
||||
$this->db = $db; |
||||
$this->limit = $limit; |
||||
$this->window = $window; |
||||
$this->burst_limit = $burst_limit; |
||||
$this->burst_refill = $burst_refill; |
||||
} |
||||
|
||||
public function check_rate_limit(string $apiKey): array { |
||||
$this->db->beginTransaction(); |
||||
|
||||
try { |
||||
$now = time(); |
||||
$stmt = $this->db->prepare( |
||||
"SELECT * FROM api_rate_limits |
||||
WHERE api_key = ? FOR UPDATE"); |
||||
$stmt->execute([$apiKey]); |
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC); |
||||
|
||||
if (!$record) { |
||||
// First request - initialize with full burst capacity |
||||
$stmt = $this->db->prepare( |
||||
"INSERT INTO api_rate_limits |
||||
(api_key, request_count, last_request, |
||||
burst_tokens, burst_last_update) |
||||
VALUES (?, 1, FROM_UNIXTIME(?), ?, ?)"); |
||||
$stmt->execute([ |
||||
$apiKey, |
||||
$now, |
||||
$this->burst_limit - 1, // Start with full burst capacity |
||||
$now |
||||
]); |
||||
|
||||
return [ |
||||
'allowed' => true, |
||||
'remaining' => $this->limit - 1, |
||||
'burst_remaining' => $this->burst_limit - 1, |
||||
'reset' => $now + $this->window, |
||||
'burst_reset' => $now + ceil(1 / $this->burst_refill) |
||||
]; |
||||
} |
||||
|
||||
// Calculate burst token refill |
||||
$last_burst_update = strtotime($record['burst_last_update']); |
||||
$time_passed = $now - $last_burst_update; |
||||
$tokens_to_add = $time_passed * $this->burst_refill; |
||||
$burst_tokens = min( |
||||
$this->burst_limit, |
||||
$record['burst_tokens'] + $tokens_to_add |
||||
); |
||||
|
||||
// Check burst limit first |
||||
if ($burst_tokens < 1) { |
||||
$this->db->commit(); |
||||
return [ |
||||
'allowed' => false, |
||||
'remaining' => $this->limit - $record['request_count'], |
||||
'burst_remaining' => 0, |
||||
'reset' => strtotime($record['last_request']) + $this->window, |
||||
'burst_reset' => $now + ceil((1 - $burst_tokens) / $this->burst_refill) |
||||
]; |
||||
} |
||||
|
||||
// Check long-term limit |
||||
$windowStart = $now - $this->window; |
||||
$lastRequest = strtotime($record['last_request']); |
||||
|
||||
if ($lastRequest < $windowStart) { |
||||
// New time window, reset count |
||||
$stmt = $this->db->prepare( |
||||
"UPDATE api_rate_limits |
||||
SET request_count = 1, |
||||
last_request = FROM_UNIXTIME(?), |
||||
burst_tokens = ?, |
||||
burst_last_update = FROM_UNIXTIME(?) |
||||
WHERE api_key = ?"); |
||||
$stmt->execute([ |
||||
$now, |
||||
$burst_tokens - 1, |
||||
$now, |
||||
$apiKey |
||||
]); |
||||
|
||||
$remaining = $this->limit - 1; |
||||
} else { |
||||
// Within current window |
||||
if ($record['request_count'] >= $this->limit) { |
||||
$this->db->commit(); |
||||
return [ |
||||
'allowed' => false, |
||||
'remaining' => 0, |
||||
'burst_remaining' => $burst_tokens, |
||||
'reset' => $lastRequest + $this->window, |
||||
'burst_reset' => $now + ceil((1 - $burst_tokens) / $this->burst_refill) |
||||
]; |
||||
} |
||||
|
||||
// Update counts |
||||
$stmt = $this->db->prepare( |
||||
"UPDATE api_rate_limits |
||||
SET request_count = request_count + 1, |
||||
last_request = FROM_UNIXTIME(?), |
||||
burst_tokens = ?, |
||||
burst_last_update = FROM_UNIXTIME(?) |
||||
WHERE api_key = ?"); |
||||
$stmt->execute([ |
||||
$now, |
||||
$burst_tokens - 1, |
||||
$now, |
||||
$apiKey |
||||
]); |
||||
|
||||
$remaining = $this->limit - ($record['request_count'] + 1); |
||||
} |
||||
|
||||
$this->db->commit(); |
||||
return [ |
||||
'allowed' => true, |
||||
'remaining' => $remaining, |
||||
'burst_remaining' => $burst_tokens - 1, |
||||
'reset' => $now + ($this->window - ($now - $windowStart)), |
||||
'burst_reset' => $now + ceil((1 - ($burst_tokens - 1)) / $this->burst_refill) |
||||
]; |
||||
} catch (\Exception $e) { |
||||
$this->db->rollBack(); |
||||
throw $e; |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,111 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\apis\rate_limiting; |
||||
|
||||
class redis_rate_limiter { |
||||
private $redis; |
||||
private $limit; |
||||
private $window; |
||||
private $burst_limit; |
||||
private $burst_refill; |
||||
|
||||
public function __construct( |
||||
\Redis $redis, |
||||
int $limit = 100, |
||||
int $window = 3600, |
||||
int $burst_limit = 5, |
||||
int $burst_refill = 1 |
||||
) { |
||||
$this->redis = $redis; |
||||
$this->limit = $limit; |
||||
$this->window = $window; |
||||
$this->burst_limit = $burst_limit; |
||||
$this->burst_refill = $burst_refill; |
||||
} |
||||
|
||||
public function check_rate_limit(string $api_key): array { |
||||
$now = time(); |
||||
$long_term_key = "rate_limit:long:$api_key"; |
||||
$burst_key = "rate_limit:burst:$api_key"; |
||||
|
||||
// Long-term rate limiting |
||||
$long_term = $this->redis->get($long_term_key); |
||||
if (!$long_term) { |
||||
$this->redis->setex($long_term_key, $this->window, 1); |
||||
$long_remaining = $this->limit - 1; |
||||
$long_reset = $now + $this->window; |
||||
} else { |
||||
$long_count = (int)$long_term; |
||||
if ($long_count >= $this->limit) { |
||||
$longTtl = $this->redis->ttl($long_term_key); |
||||
return [ |
||||
'allowed' => false, |
||||
'remaining' => 0, |
||||
'burst_remaining' => 0, |
||||
'reset' => $now + $longTtl, |
||||
'burst_reset' => $now + $longTtl, |
||||
'limit_type' => 'long' |
||||
]; |
||||
} |
||||
$this->redis->incr($long_term_key); |
||||
$long_remaining = $this->limit - ($long_count + 1); |
||||
$long_reset = $now + $this->redis->ttl($long_term_key); |
||||
} |
||||
|
||||
// Burst rate limiting (token bucket) |
||||
$burst_data = $this->redis->hGetAll($burst_key); |
||||
if (empty($burst_data)) { |
||||
$burst_tokens = $this->burst_limit - 1; |
||||
$burst_last_update = $now; |
||||
$this->redis->hMSet($burst_key, [ |
||||
'tokens' => $burst_tokens, |
||||
'last_update' => $now |
||||
]); |
||||
$this->redis->expire($burst_key, $this->window); |
||||
} else { |
||||
$burst_tokens = (int)$burst_data['tokens']; |
||||
$burst_last_update = (int)$burst_data['last_update']; |
||||
|
||||
// Refill tokens based on time passed |
||||
$time_passed = $now - $burst_last_update; |
||||
$tokens_to_add = $time_passed * $this->burst_refill; |
||||
$burst_tokens = min( |
||||
$this->burst_limit, |
||||
$burst_tokens + $tokens_to_add |
||||
); |
||||
|
||||
if ($burst_tokens < 1) { |
||||
return [ |
||||
'allowed' => false, |
||||
'remaining' => $long_remaining, |
||||
'burst_remaining' => 0, |
||||
'reset' => $long_reset, |
||||
'burst_reset' => $now + ceil((1 - $burst_tokens) / $this->burst_refill), |
||||
'limit_type' => 'burst' |
||||
]; |
||||
} |
||||
|
||||
$burst_tokens--; |
||||
$this->redis->hMSet($burst_key, [ |
||||
'tokens' => $burst_tokens, |
||||
'last_update' => $now |
||||
]); |
||||
$this->redis->expire($burst_key, $this->window); |
||||
} |
||||
|
||||
return [ |
||||
'allowed' => true, |
||||
'remaining' => $long_remaining, |
||||
'burst_remaining' => $burst_tokens, |
||||
'reset' => $long_reset, |
||||
'burst_reset' => $now + ceil((1 - $burst_tokens) / $this->burst_refill) |
||||
]; |
||||
} |
||||
} |
||||
@ -0,0 +1,109 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\http; |
||||
|
||||
class container { |
||||
protected array $bindings = []; |
||||
protected array $instances = []; |
||||
|
||||
public function bind(string $abstract, $concrete = null, bool $shared = false): void |
||||
{ |
||||
if ($concrete === null) { |
||||
$concrete = $abstract; |
||||
} |
||||
$this->bindings[$abstract] = [ |
||||
'concrete' => $concrete, |
||||
'shared' => $shared |
||||
]; |
||||
} |
||||
|
||||
public function singleton(string $abstract, $concrete = null): void |
||||
{ |
||||
$this->bind($abstract, $concrete, true); |
||||
} |
||||
|
||||
public function make(string $abstract, array $parameters = []) |
||||
{ |
||||
// Return if already resolved as singleton |
||||
if (isset($this->instances[$abstract])) { |
||||
return $this->instances[$abstract]; |
||||
} |
||||
|
||||
// Get the concrete implementation |
||||
$concrete = $this->bindings[$abstract]['concrete'] ?? $abstract; |
||||
|
||||
// If it's a closure, resolve it |
||||
if ($concrete instanceof \Closure) { |
||||
$object = $concrete($this, $parameters); |
||||
} elseif (is_string($concrete)) { |
||||
// If it's a class name, instantiate it |
||||
$object = $this->build($concrete, $parameters); |
||||
} else { // Otherwise use as-is |
||||
$object = $concrete; |
||||
} |
||||
|
||||
// Store if singleton |
||||
if (($this->bindings[$abstract]['shared'] ?? false) === true) { |
||||
$this->instances[$abstract] = $object; |
||||
} |
||||
|
||||
return $object; |
||||
} |
||||
|
||||
protected function build(string $class, array $parameters = []) |
||||
{ |
||||
$reflector = new \ReflectionClass($class); |
||||
|
||||
// Check if class is instantiable |
||||
if (!$reflector->isInstantiable()) { |
||||
throw new \Exception("Class {$class} is not instantiable"); |
||||
} |
||||
|
||||
// Get the constructor |
||||
$constructor = $reflector->getConstructor(); |
||||
|
||||
// If no constructor, instantiate without arguments |
||||
if ($constructor === null) { |
||||
return new $class(); |
||||
} |
||||
|
||||
// Get constructor parameters |
||||
$dependencies = $constructor->getParameters(); |
||||
$instances = $this->resolve_dependencies($dependencies, $parameters); |
||||
|
||||
return $reflector->newInstanceArgs($instances); |
||||
} |
||||
|
||||
protected function resolve_dependencies(array $dependencies, array $parameters = []) |
||||
{ |
||||
$results = []; |
||||
|
||||
foreach ($dependencies as $dependency) { |
||||
// Check if parameter was provided |
||||
if (array_key_exists($dependency->name, $parameters)) { |
||||
$results[] = $parameters[$dependency->name]; |
||||
continue; |
||||
} |
||||
|
||||
// Get the type hinted class |
||||
$type = $dependency->getType(); |
||||
|
||||
if ($type && !$type->isBuiltin()) { |
||||
$results[] = $this->make($type->getName()); |
||||
} elseif ($dependency->isDefaultValueAvailable()) { |
||||
$results[] = $dependency->getDefaultValue(); |
||||
} else { |
||||
throw new \Exception("Cannot resolve dependency {$dependency->name}"); |
||||
} |
||||
} |
||||
|
||||
return $results; |
||||
} |
||||
} |
||||
@ -0,0 +1,100 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\http; |
||||
|
||||
class kernel { |
||||
protected container $container; |
||||
protected array $middleware = []; |
||||
protected array $service_providers = []; |
||||
|
||||
public function __construct() { |
||||
$this->container = new container(); |
||||
$this->register_base_bindings(); |
||||
} |
||||
|
||||
protected function register_base_bindings(): void { |
||||
$this->container->singleton(container::class, fn() => $this->container); |
||||
$this->container->singleton(kernel::class, fn() => $this); |
||||
$this->container->bind(request::class, fn() => request::create_from_globals()); |
||||
} |
||||
|
||||
public function get_container(): container { |
||||
return $this->container; |
||||
} |
||||
|
||||
public function register_service_provider(string $provider_class): void |
||||
{ |
||||
$provider = new $provider_class($this); |
||||
$provider->register(); |
||||
$this->service_providers[] = $provider; |
||||
} |
||||
|
||||
public function add_middleware($middleware): void { |
||||
$this->middleware[] = $middleware; |
||||
} |
||||
|
||||
public function handle(Request $request): Response { |
||||
try { |
||||
$response = $this->send_request_through_middleware($request); |
||||
return $response; |
||||
} catch (\Throwable $e) { |
||||
return $this->handle_exception($e); |
||||
} |
||||
} |
||||
|
||||
protected function send_request_through_middleware(Request $request): Response |
||||
{ |
||||
$middleware_stack = $this->build_middleware_stack(); |
||||
|
||||
// Create initial response |
||||
$response = new Response(); |
||||
|
||||
// Process the request through middleware |
||||
return $middleware_stack($request, $response); |
||||
} |
||||
|
||||
protected function build_middleware_stack(): callable { |
||||
return array_reduce( |
||||
array_reverse($this->middleware), |
||||
function($next, $middleware) { |
||||
return function($request, $response) use ($next, $middleware) { |
||||
// Resolve middleware if it's a class name |
||||
if (is_string($middleware) && class_exists($middleware)) { |
||||
$middleware = $this->container->make($middleware); |
||||
} |
||||
|
||||
if (is_callable($middleware)) { |
||||
return $middleware($request, $response, $next); |
||||
} |
||||
|
||||
throw new \Exception("Invalid middleware"); |
||||
}; |
||||
}, |
||||
function($request, $response) { |
||||
return $response; |
||||
} |
||||
); |
||||
} |
||||
|
||||
protected function handle_exception(\Throwable $e): Response { |
||||
// Basic exception handling - override in child class |
||||
$response = new response(); |
||||
return $response |
||||
->set_status_code(500) |
||||
->set_content('Server Error: ' . $e->getMessage()); |
||||
} |
||||
|
||||
public function run(): void |
||||
{ |
||||
$request = $this->container->make(request::class); |
||||
$response = $this->handle($request); |
||||
$response->send(); |
||||
} |
||||
} |
||||
@ -0,0 +1,67 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\http; |
||||
|
||||
class request |
||||
{ |
||||
protected array $query_params; |
||||
protected array $post_data; |
||||
protected array $server; |
||||
protected array $cookies; |
||||
protected array $files; |
||||
protected array $headers; |
||||
|
||||
public function __construct( |
||||
array $query_params = [], |
||||
array $post_data = [], |
||||
array $server = [], |
||||
array $cookies = [], |
||||
array $files = [], |
||||
array $headers = [] |
||||
) { |
||||
$this->query_params = $query_params; |
||||
$this->post_data = $post_data; |
||||
$this->server = $server; |
||||
$this->cookies = $cookies; |
||||
$this->files = $files; |
||||
$this->headers = $headers; |
||||
} |
||||
|
||||
public static function create_from_globals(): self { |
||||
return new self( |
||||
$_GET, |
||||
$_POST, |
||||
$_SERVER, |
||||
$_COOKIE, |
||||
$_FILES, |
||||
getallheaders() |
||||
); |
||||
} |
||||
|
||||
public function get_method(): string { |
||||
return strtoupper($this->server['REQUEST_METHOD'] ?? 'GET'); |
||||
} |
||||
|
||||
public function get_uri(): string { |
||||
return $this->server['REQUEST_URI'] ?? '/'; |
||||
} |
||||
|
||||
public function get_query_params(): array { |
||||
return $this->query_params; |
||||
} |
||||
|
||||
public function get_post_data(): array { |
||||
return $this->post_data; |
||||
} |
||||
|
||||
public function get_header(string $name): ?string { |
||||
return $this->headers[$name] ?? null; |
||||
} |
||||
} |
||||
@ -0,0 +1,52 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace CodeHydrater\http; |
||||
|
||||
class response |
||||
{ |
||||
protected string $content; |
||||
protected int $status_code; |
||||
protected array $headers; |
||||
|
||||
public function __construct( |
||||
string $content = '', |
||||
int $status_code = 200, |
||||
array $headers = [] |
||||
) { |
||||
$this->content = $content; |
||||
$this->status_code = $status_code; |
||||
$this->headers = $headers; |
||||
} |
||||
|
||||
public function send(): void { |
||||
http_response_code($this->status_code); |
||||
|
||||
foreach ($this->headers as $name => $value) { |
||||
header("$name: $value"); |
||||
} |
||||
|
||||
echo $this->content; |
||||
} |
||||
|
||||
public function set_content(string $content): self { |
||||
$this->content = $content; |
||||
return $this; |
||||
} |
||||
|
||||
public function set_status_code(int $code): self { |
||||
$this->status_code = $code; |
||||
return $this; |
||||
} |
||||
|
||||
public function add_header(string $name, string $value): self { |
||||
$this->headers[$name] = $value; |
||||
return $this; |
||||
} |
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
|
||||
namespace CodeHydrater\http; |
||||
|
||||
abstract class service_provider { |
||||
protected kernel $kernel; |
||||
|
||||
public function __construct(kernel $kernel) |
||||
{ |
||||
$this->kernel = $kernel; |
||||
} |
||||
|
||||
abstract public function register(): void; |
||||
} |
||||
|
||||
/** |
||||
* Example Useage: |
||||
<?php |
||||
|
||||
namespace App\Providers; |
||||
|
||||
use CodeHydrater\http\kernel; |
||||
use CodeHydrater\http\service_provider; |
||||
use App\Services\Database; |
||||
use App\Services\Logger; |
||||
|
||||
class app_service_provider extends service_provider |
||||
{ |
||||
public function register(): void |
||||
{ |
||||
$this->kernel->get_container()->singleton(Database::class, function() { |
||||
return new Database( |
||||
getenv('DB_HOST'), |
||||
getenv('DB_USER'), |
||||
getenv('DB_PASS'), |
||||
getenv('DB_NAME') |
||||
); |
||||
}); |
||||
|
||||
$this->kernel->get_container()->singleton(Logger::class, function() { |
||||
return new Logger(__DIR__ . '/../storage/logs/app.log'); |
||||
}); |
||||
} |
||||
} |
||||
*/ |
||||
Loading…
Reference in new issue