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