From 809a95f4eb122518f300471d2f311d2d4c3efd7e Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 1 Aug 2025 10:37:33 -0400 Subject: [PATCH] extras http and rate_limiting... --- .../apis/rate_limiting/api_authenticator.php | 72 ++++++++ .../create_subscription_aware_validator.php | 151 +++++++++++++++++ .../apis/rate_limiting/rate_limiter.php | 155 ++++++++++++++++++ .../apis/rate_limiting/redis_rate_limiter.php | 111 +++++++++++++ src/classes/http/container.php | 109 ++++++++++++ src/classes/http/kernel.php | 100 +++++++++++ src/classes/http/request.php | 67 ++++++++ src/classes/http/response.php | 52 ++++++ src/classes/http/service_provider.php | 53 ++++++ 9 files changed, 870 insertions(+) create mode 100644 src/classes/apis/rate_limiting/api_authenticator.php create mode 100644 src/classes/apis/rate_limiting/create_subscription_aware_validator.php create mode 100644 src/classes/apis/rate_limiting/rate_limiter.php create mode 100644 src/classes/apis/rate_limiting/redis_rate_limiter.php create mode 100644 src/classes/http/container.php create mode 100644 src/classes/http/kernel.php create mode 100644 src/classes/http/request.php create mode 100644 src/classes/http/response.php create mode 100644 src/classes/http/service_provider.php diff --git a/src/classes/apis/rate_limiting/api_authenticator.php b/src/classes/apis/rate_limiting/api_authenticator.php new file mode 100644 index 0000000..9d86a98 --- /dev/null +++ b/src/classes/apis/rate_limiting/api_authenticator.php @@ -0,0 +1,72 @@ + + * @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; + } +} diff --git a/src/classes/apis/rate_limiting/create_subscription_aware_validator.php b/src/classes/apis/rate_limiting/create_subscription_aware_validator.php new file mode 100644 index 0000000..4164b1c --- /dev/null +++ b/src/classes/apis/rate_limiting/create_subscription_aware_validator.php @@ -0,0 +1,151 @@ + + * @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) +); +"; + } + +} diff --git a/src/classes/apis/rate_limiting/rate_limiter.php b/src/classes/apis/rate_limiting/rate_limiter.php new file mode 100644 index 0000000..ba2744e --- /dev/null +++ b/src/classes/apis/rate_limiting/rate_limiter.php @@ -0,0 +1,155 @@ + + * @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; + } + } + + +} \ No newline at end of file diff --git a/src/classes/apis/rate_limiting/redis_rate_limiter.php b/src/classes/apis/rate_limiting/redis_rate_limiter.php new file mode 100644 index 0000000..072c07f --- /dev/null +++ b/src/classes/apis/rate_limiting/redis_rate_limiter.php @@ -0,0 +1,111 @@ + + * @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) + ]; + } +} diff --git a/src/classes/http/container.php b/src/classes/http/container.php new file mode 100644 index 0000000..67a14a0 --- /dev/null +++ b/src/classes/http/container.php @@ -0,0 +1,109 @@ + + * @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; + } +} \ No newline at end of file diff --git a/src/classes/http/kernel.php b/src/classes/http/kernel.php new file mode 100644 index 0000000..55fcc62 --- /dev/null +++ b/src/classes/http/kernel.php @@ -0,0 +1,100 @@ + + * @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(); + } +} \ No newline at end of file diff --git a/src/classes/http/request.php b/src/classes/http/request.php new file mode 100644 index 0000000..8452236 --- /dev/null +++ b/src/classes/http/request.php @@ -0,0 +1,67 @@ + + * @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; + } +} \ No newline at end of file diff --git a/src/classes/http/response.php b/src/classes/http/response.php new file mode 100644 index 0000000..f91b227 --- /dev/null +++ b/src/classes/http/response.php @@ -0,0 +1,52 @@ + + * @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; + } +} \ No newline at end of file diff --git a/src/classes/http/service_provider.php b/src/classes/http/service_provider.php new file mode 100644 index 0000000..4fa2359 --- /dev/null +++ b/src/classes/http/service_provider.php @@ -0,0 +1,53 @@ + + * @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: +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'); + }); + } +} + */ \ No newline at end of file