once(Environment::class, fn(): Environment => Environment::create(static::fromBase(".env"))); } /** * Bootstrap the application for CLI mode. */ public static function cli(string $basePath): void { static::base($basePath); Container::instance()->once(Argument::class, fn(): Argument => Argument::create()); } /** * Bootstrap the application for HTTP mode. */ public static function http(string $basePath): void { static::base($basePath); Container::instance()->once(Request::class, fn(): Request => Request::create()); Container::instance()->once(Response::class); Container::instance()->once(Router::class); if (class_exists(Jwt::class)) { Container::instance()->once(Jwt::class, fn(): Jwt => Jwt::create(Container::instance()->get(Environment::class))); } if (class_exists(Session::class)) { Container::instance()->once(Session::class, fn(): Session => Session::create()); } } /** * Return full path from the $basePath passed into the cli() or http() factory. */ public static function fromBase(string $path): string { if (static::$basePath === null) { throw new FrameworkException("Application base path not initialized."); } return static::$basePath . ltrim($path, "/"); } /** * Run the application and handle the request. */ public static function run(): void { if (PHP_SAPI === "cli") { exit(1); } $request = Container::instance()->get(Request::class); $response = Container::instance()->get(Response::class); try { Container::instance()->get(Router::class)->dispatch($request, $response)->send(); } catch (Throwable $throwable) { if (class_exists(HttpException::class) && is_a($throwable, HttpException::class)) { $response ->setStatus($throwable->getCode() ?: 500) ->setBody($throwable->getMessage()) ->send(); } else { error_log($throwable->getMessage()); $response->setStatus(500)->setBody("Internal Server Error")->send(); } } } } final class Container { protected const BOUND_CLASS = "__new__"; protected const BOUND_FACTORY = "__call__"; protected const BOUND_ALIAS = "__alias__"; private static $instance; private array $bindings = []; private array $cache = []; /** * Get the container singleton instance. */ public static function instance(): static { return static::$instance ??= new self(); } /** * Bind a class or factory to the container. * * @template T of object * @param class-string|string $id * @param callable():T|T|class-string|string|null $concrete */ public function bind(string $id, callable|object|string|null $concrete = null): void { $concrete ??= $id; switch (true) { case is_string($concrete) && isset($this->bindings[$concrete]): $this->bindings[$id] = [self::BOUND_ALIAS, $concrete]; break; case is_string($concrete) && class_exists($concrete, true): $this->bindings[$id] = [self::BOUND_CLASS, $concrete]; break; case is_callable($concrete): $this->bindings[$id] = [self::BOUND_FACTORY, $concrete]; break; case is_object($concrete): $this->cache[$id] = $concrete; break; default: throw new FrameworkException(sprintf("Service [%s] cannot be bound.", $id)); } } /** * Bind a singleton to the container. * * @template T of object * @param class-string|string $id * @param callable():T|T|class-string|null $concrete */ public function once(string $id, callable|object|string|null $concrete = null): void { $this->cache[$id] = null; $this->bind($id, $concrete); } /** * Resolve a class or binding from the container. * * @template T of object * @template U of class-string|string * @param U $id * @param array|list $dependencies * @return (U is class-string ? T : object) */ public function get(string $id, array $dependencies = []): object { if (isset($this->cache[$id])) { return $this->cache[$id]; } if (!array_key_exists($id, $this->bindings)) { if (class_exists($id, true)) { try { return new $id(...$dependencies); } catch (Throwable $e) { throw new FrameworkException(sprintf("Service [%s] could not be instantiated.", $id), 0, $e); } } throw new FrameworkException(sprintf("Service [%s] is not bound and cannot be instantiated.", $id)); } [$type, $binding] = $this->bindings[$id]; if ($type === self::BOUND_ALIAS) { return $this->get($binding, $dependencies); } try { $resolved = $type === self::BOUND_CLASS ? new $binding(...$dependencies) : $binding($this, ...$dependencies); } catch (Throwable $throwable) { throw new FrameworkException(sprintf("Service [%s] could not be instantiated.", $id), 0, $throwable); } if (!is_object($resolved)) { throw new FrameworkException(sprintf("Service [%s] did not resolve to an object.", $id)); } if (array_key_exists($id, $this->cache)) { $this->cache[$id] = $resolved; } return $resolved; } } class Environment { public function __construct(protected array $data = []) {} /** * Load and parse environment variables from a .env file. */ public static function create(?string $file = null): static { $file ??= Application::fromBase(".env"); if (!file_exists($file)) { return new static(); } $data = []; foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $line = trim($line); if ($line === "" || $line[0] === "#" || !str_contains($line, "=")) { continue; } /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$key, $value] = explode("=", $line, 2); $data[trim($key)] = static::autoCast(trim($value)); } return new static($data); } /** * Get a value from the environment data. */ public function get(string $key): mixed { return $this->data[$key] ?? null; } /** * Convert a string to a native type if possible. */ protected static function autoCast(string $value): mixed { if (preg_match('/^(["\']).*\1$/', $value)) { return substr($value, 1, -1); } $lower = strtolower($value); return match (true) { $lower === "true" => true, $lower === "false" => false, $lower === "null" => null, $lower === "" => null, is_numeric($value) => preg_match("/[e\.]/", $value) ? (float) $value : (int) $value, default => $value, }; } } class FrameworkException extends Exception {} class Argument { /** * @param array $arguments */ public function __construct(public readonly string $command = "", protected array $arguments = []) {} /** * Parse CLI arguments into command and options. * * @param list|null $argv */ public static function create(?array $argv = null): static { $argv ??= $_SERVER["argv"] ?? []; if (count($argv) <= 1) { return new static(); } array_shift($argv); $command = ""; $arguments = []; while (($arg = array_shift($argv)) !== null) { if ($arg === "--") { $arguments = array_merge($arguments, array_map(static::autoCast(...), $argv)); break; } if (str_starts_with((string) $arg, "--")) { $option = substr((string) $arg, 2); if (mb_stripos($option, "=") !== false) { /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$key, $value] = explode("=", $option, 2); } elseif (isset($argv[0]) && $argv[0][0] !== "-") { $key = $option; $value = array_shift($argv); } else { $key = $option; $value = true; } $arguments[$key] = static::autoCast($value); continue; } if ($arg[0] === "-") { $key = $arg[1]; $value = substr((string) $arg, 2); if ($value === "") { $value = isset($argv[0]) && $argv[0][0] !== "-" ? array_shift($argv) : true; } $arguments[$key] = static::autoCast($value); continue; } if (empty($command)) { $command = $arg; } else { $arguments[] = static::autoCast($arg); } } return new static($command, $arguments); } /** * Get an argument by key or index. */ public function get(int|string $key): mixed { return $this->arguments[$key] ?? null; } /** * Convert a string to a native type if possible. */ protected static function autoCast(string|bool $value): mixed { if (!is_string($value)) { return $value; } if (preg_match('/^(["\']).*\1$/', $value)) { return substr($value, 1, -1); } $lower = strtolower($value); return match (true) { $lower === "true" => true, $lower === "false" => false, $lower === "null" => null, is_numeric($value) => preg_match("/[e\.]/", $value) ? (float) $value : (int) $value, default => $value, }; } } /** * Resolve a class from the container. * * @template T of object * @template U of class-string|string * @param U $id * @return (U is class-string ? T : object) */ function app(string $id): object { return Container::instance()->get($id); } /** * Instantiate a class with dependencies. * * @template T of object * @template U of class-string|string * @param U $id * @param array|list $dependencies * @return (U is class-string ? T : object) */ function make(string $id, array $dependencies = []): object { return Container::instance()->get($id, $dependencies); } /** * Register a binding into the container. * * @template T of object * @param class-string|string $id * @param callable():T|T|class-string|string|null $concrete */ function bind(string $id, callable|string|null $concrete = null): void { Container::instance()->bind($id, $concrete); } /** * Register a singleton into the container. * * @template T of object * @param class-string|string $id * @param callable():T|T|class-string|null $concrete */ function once(string $id, callable|string|null $concrete = null): void { Container::instance()->once($id, $concrete); } /** * Get the absolute path from base path. */ function base_path(string $path): string { return Application::fromBase($path); } /** * Get environment variable from .env file. */ function env(string $key): mixed { return app(Environment::class)->get($key); } /** * Conditionally throw an exception. * * @throws Throwable */ function throw_if(bool $condition, Throwable|string $e): void { if ($condition) { throw $e instanceof Throwable ? $e : new Exception($e); } } /** * Get CLI argument by key. */ function arg(int|string $key): mixed { return app(Argument::class)->get($key); } /** * Register and execute a CLI command. */ function command(string $name, callable $handle): void { if (($argument = app(Argument::class))->command === $name) { exit(is_int($result = $handle($argument)) ? $result : 0); } }