Le commit un peu tardif de l'api

This commit is contained in:
Fauvet Matthis
2024-06-27 10:07:19 +02:00
parent a0b5103978
commit 35a6e4266f
22 changed files with 4531 additions and 0 deletions

942
api/flight/Engine.php Normal file

File diff suppressed because it is too large Load Diff

146
api/flight/Flight.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
use flight\Engine;
use flight\net\Request;
use flight\net\Response;
use flight\net\Router;
use flight\template\View;
use flight\net\Route;
require_once __DIR__ . '/autoload.php';
/**
* The Flight class is a static representation of the framework.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*
* # Core methods
* @method static void start() Starts the framework.
* @method static void path(string $path) Adds a path for autoloading classes.
* @method static void stop(?int $code = null) Stops the framework and sends a response.
* @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true)
* Stop the framework with an optional status code and message.
* @method static void register(string $name, string $class, array $params = [], ?callable $callback = null)
* Registers a class to a framework method.
* @method static void unregister(string $methodName)
* Unregisters a class to a framework method.
* @method static void registerContainerHandler(callable|object $containerHandler) Registers a container handler.
*
* # Routing
* @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Maps a URL pattern to a callback with all applicable methods.
* @method static void group(string $pattern, callable $callback, callable[] $group_middlewares = [])
* Groups a set of routes together under a common prefix.
* @method static Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a POST URL to a callback function.
* @method static Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a PUT URL to a callback function.
* @method static Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a PATCH URL to a callback function.
* @method static Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a DELETE URL to a callback function.
* @method static Router router() Returns Router instance.
* @method static string getUrl(string $alias, array<string, mixed> $params = []) Gets a url from an alias
*
* @method static void map(string $name, callable $callback) Creates a custom framework method.
*
* @method static void before(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* Adds a filter before a framework method.
* @method static void after(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* Adds a filter after a framework method.
*
* @method static void set(string|iterable<string, mixed> $key, mixed $value) Sets a variable.
* @method static mixed get(?string $key) Gets a variable.
* @method static bool has(string $key) Checks if a variable is set.
* @method static void clear(?string $key = null) Clears a variable.
*
* # Views
* @method static void render(string $file, ?array<string, mixed> $data = null, ?string $key = null)
* Renders a template file.
* @method static View view() Returns View instance.
*
* # Request-Response
* @method static Request request() Returns Request instance.
* @method static Response response() Returns Response instance.
* @method static void redirect(string $url, int $code = 303) Redirects to another URL.
* @method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* Sends a JSON response.
* @method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* Sends a JSON response and immediately halts the request.
* @method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* Sends a JSONP response.
* @method static void error(Throwable $exception) Sends an HTTP 500 response.
* @method static void notFound() Sends an HTTP 404 response.
*
* # HTTP caching
* @method static void etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching.
* @method static void lastModified(int $time) Performs last modified HTTP caching.
*/
class Flight
{
/** Framework engine. */
private static Engine $engine;
/** Whether or not the app has been initialized. */
private static bool $initialized = false;
/**
* Don't allow object instantiation
*
* @codeCoverageIgnore
* @return void
*/
private function __construct()
{
}
/**
* Forbid cloning the class
*
* @codeCoverageIgnore
* @return void
*/
private function __clone()
{
}
/**
* Handles calls to static methods.
*
* @param string $name Method name
* @param array<int, mixed> $params Method parameters
*
* @return mixed Callback results
* @throws Exception
*/
public static function __callStatic(string $name, array $params)
{
return self::app()->{$name}(...$params);
}
/** @return Engine Application instance */
public static function app(): Engine
{
if (!self::$initialized) {
require_once __DIR__ . '/autoload.php';
self::setEngine(new Engine());
self::$initialized = true;
}
return self::$engine;
}
/**
* Set the engine instance
*
* @param Engine $engine Vroom vroom!
*/
public static function setEngine(Engine $engine): void
{
self::$engine = $engine;
}
}

10
api/flight/autoload.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use flight\core\Loader;
require_once __DIR__ . '/Flight.php';
require_once __DIR__ . '/core/Loader.php';
Loader::autoload(true, [dirname(__DIR__)]);

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace flight\commands;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PhpNamespace;
class ControllerCommand extends AbstractBaseCommand
{
/**
* Construct
*
* @param array<string,mixed> $config JSON config from .runway-config.json
*/
public function __construct(array $config)
{
parent::__construct('make:controller', 'Create a controller', $config);
$this->argument('<controller>', 'The name of the controller to create (with or without the Controller suffix)');
}
/**
* Executes the function
*
* @return void
*/
public function execute(string $controller)
{
$io = $this->app()->io();
if (isset($this->config['app_root']) === false) {
$io->error('app_root not set in .runway-config.json', true);
return;
}
if (!preg_match('/Controller$/', $controller)) {
$controller .= 'Controller';
}
$controllerPath = getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controller . '.php';
if (file_exists($controllerPath) === true) {
$io->error($controller . ' already exists.', true);
return;
}
if (is_dir(dirname($controllerPath)) === false) {
$io->info('Creating directory ' . dirname($controllerPath), true);
mkdir(dirname($controllerPath), 0755, true);
}
$file = new PhpFile();
$file->setStrictTypes();
$namespace = new PhpNamespace('app\\controllers');
$namespace->addUse('flight\\Engine');
$class = new ClassType($controller);
$class->addProperty('app')
->setVisibility('protected')
->setType('flight\\Engine')
->addComment('@var Engine');
$method = $class->addMethod('__construct')
->addComment('Constructor')
->setVisibility('public')
->setBody('$this->app = $app;');
$method->addParameter('app')
->setType('flight\\Engine');
$namespace->add($class);
$file->addNamespace($namespace);
$this->persistClass($controller, $file);
$io->ok('Controller successfully created at ' . $controllerPath, true);
}
/**
* Saves the class name to a file
*
* @param string $controllerName Name of the Controller
* @param PhpFile $file Class Object from Nette\PhpGenerator
*
* @return void
*/
protected function persistClass(string $controllerName, PhpFile $file)
{
$printer = new \Nette\PhpGenerator\PsrPrinter();
file_put_contents(getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controllerName . '.php', $printer->printFile($file));
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace flight\commands;
use Flight;
use flight\net\Route;
/**
* @property-read ?bool $get
* @property-read ?bool $post
* @property-read ?bool $delete
* @property-read ?bool $put
* @property-read ?bool $patch
*/
class RouteCommand extends AbstractBaseCommand
{
/**
* Construct
*
* @param array<string,mixed> $config JSON config from .runway-config.json
*/
public function __construct(array $config)
{
parent::__construct('routes', 'Gets all routes for an application', $config);
$this->option('--get', 'Only return GET requests');
$this->option('--post', 'Only return POST requests');
$this->option('--delete', 'Only return DELETE requests');
$this->option('--put', 'Only return PUT requests');
$this->option('--patch', 'Only return PATCH requests');
}
/**
* Executes the function
*
* @return void
*/
public function execute()
{
$io = $this->app()->io();
if (isset($this->config['index_root']) === false) {
$io->error('index_root not set in .runway-config.json', true);
return;
}
$io->bold('Routes', true);
$cwd = getcwd();
$index_root = $cwd . '/' . $this->config['index_root'];
// This makes it so the framework doesn't actually execute
Flight::map('start', function () {
return;
});
include($index_root);
$routes = Flight::router()->getRoutes();
$arrayOfRoutes = [];
foreach ($routes as $route) {
if ($this->shouldAddRoute($route) === true) {
$middlewares = [];
if (!empty($route->middleware)) {
try {
$middlewares = array_map(function ($middleware) {
$middleware_class_name = explode("\\", get_class($middleware));
return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name);
}, $route->middleware);
} catch (\TypeError $e) {
$middlewares[] = 'Bad Middleware';
} finally {
if (is_string($route->middleware) === true) {
$middlewares[] = $route->middleware;
}
}
}
$arrayOfRoutes[] = [
'Pattern' => $route->pattern,
'Methods' => implode(', ', $route->methods),
'Alias' => $route->alias ?? '',
'Streamed' => $route->is_streamed ? 'Yes' : 'No',
'Middleware' => !empty($middlewares) ? implode(",", $middlewares) : '-'
];
}
}
$io->table($arrayOfRoutes, [
'head' => 'boldGreen'
]);
}
/**
* Whether or not to add the route based on the request
*
* @param Route $route Flight Route object
*
* @return boolean
*/
public function shouldAddRoute(Route $route)
{
$boolval = false;
$showAll = !$this->get && !$this->post && !$this->put && !$this->delete && !$this->patch;
if ($showAll === true) {
$boolval = true;
} else {
$methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ];
foreach ($methods as $method) {
$lowercaseMethod = strtolower($method);
if (
$this->{$lowercaseMethod} === true &&
(
$route->methods[0] === '*' ||
in_array($method, $route->methods, true) === true
)
) {
$boolval = true;
break;
}
}
}
return $boolval;
}
}

File diff suppressed because it is too large Load Diff

241
api/flight/core/Loader.php Normal file
View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace flight\core;
use Closure;
use Exception;
/**
* The Loader class is responsible for loading objects. It maintains
* a list of reusable class instances and can generate a new class
* instances with custom initialization parameters. It also performs
* class autoloading.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Loader
{
/**
* Registered classes.
*
* @var array<string, array{class-string|Closure(): object, array<int, mixed>, ?callable}> $classes
*/
protected array $classes = [];
/**
* If this is disabled, classes can load with underscores
*/
protected static bool $v2ClassLoading = true;
/**
* Class instances.
*
* @var array<string, object>
*/
protected array $instances = [];
/**
* Autoload directories.
*
* @var array<int, string>
*/
protected static array $dirs = [];
/**
* Registers a class.
*
* @param string $name Registry name
* @param class-string<T>|Closure(): T $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param ?Closure(T $instance): void $callback $callback Function to call after object instantiation
*
* @template T of object
*/
public function register(string $name, $class, array $params = [], ?callable $callback = null): void
{
unset($this->instances[$name]);
$this->classes[$name] = [$class, $params, $callback];
}
/**
* Unregisters a class.
*
* @param string $name Registry name
*/
public function unregister(string $name): void
{
unset($this->classes[$name]);
}
/**
* Loads a registered class.
*
* @param string $name Method name
* @param bool $shared Shared instance
*
* @throws Exception
*
* @return ?object Class instance
*/
public function load(string $name, bool $shared = true): ?object
{
$obj = null;
if (isset($this->classes[$name])) {
[0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name];
$exists = isset($this->instances[$name]);
if ($shared) {
$obj = ($exists) ?
$this->getInstance($name) :
$this->newInstance($class, $params);
if (!$exists) {
$this->instances[$name] = $obj;
}
} else {
$obj = $this->newInstance($class, $params);
}
if ($callback && (!$shared || !$exists)) {
$ref = [&$obj];
\call_user_func_array($callback, $ref);
}
}
return $obj;
}
/**
* Gets a single instance of a class.
*
* @param string $name Instance name
*
* @return ?object Class instance
*/
public function getInstance(string $name): ?object
{
return $this->instances[$name] ?? null;
}
/**
* Gets a new instance of a class.
*
* @param class-string<T>|Closure(): class-string<T> $class Class name or callback function to instantiate class
* @param array<int, string> $params Class initialization parameters
*
* @template T of object
*
* @throws Exception
*
* @return T Class instance
*/
public function newInstance($class, array $params = [])
{
if (\is_callable($class)) {
return \call_user_func_array($class, $params);
}
return new $class(...$params);
}
/**
* Gets a registered callable
*
* @param string $name Registry name
*
* @return mixed Class information or null if not registered
*/
public function get(string $name)
{
return $this->classes[$name] ?? null;
}
/**
* Resets the object to the initial state.
*/
public function reset(): void
{
$this->classes = [];
$this->instances = [];
}
// Autoloading Functions
/**
* Starts/stops autoloader.
*
* @param bool $enabled Enable/disable autoloading
* @param string|iterable<int, string> $dirs Autoload directories
*/
public static function autoload(bool $enabled = true, $dirs = []): void
{
if ($enabled) {
spl_autoload_register([__CLASS__, 'loadClass']);
} else {
spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore
}
if (!empty($dirs)) {
self::addDirectory($dirs);
}
}
/**
* Autoloads classes.
*
* Classes are not allowed to have underscores in their names.
*
* @param string $class Class name
*/
public static function loadClass(string $class): void
{
$replace_chars = self::$v2ClassLoading === true ? ['\\', '_'] : ['\\'];
$classFile = str_replace($replace_chars, '/', $class) . '.php';
foreach (self::$dirs as $dir) {
$filePath = "$dir/$classFile";
if (file_exists($filePath)) {
require_once $filePath;
return;
}
}
}
/**
* Adds a directory for autoloading classes.
*
* @param string|iterable<int, string> $dir Directory path
*/
public static function addDirectory($dir): void
{
if (\is_array($dir) || \is_object($dir)) {
foreach ($dir as $value) {
self::addDirectory($value);
}
} elseif (\is_string($dir)) {
if (!\in_array($dir, self::$dirs, true)) {
self::$dirs[] = $dir;
}
}
}
/**
* Sets the value for V2 class loading.
*
* @param bool $value The value to set for V2 class loading.
*
* @return void
*/
public static function setV2ClassLoading(bool $value): void
{
self::$v2ClassLoading = $value;
}
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace flight\database;
use flight\util\Collection;
use PDO;
use PDOStatement;
class PdoWrapper extends PDO
{
/**
* Use this for INSERTS, UPDATES, or if you plan on using a SELECT in a while loop
*
* Ex: $statement = $db->runQuery("SELECT * FROM table WHERE something = ?", [ $something ]);
* while($row = $statement->fetch()) {
* // ...
* }
*
* $db->runQuery("INSERT INTO table (name) VALUES (?)", [ $name ]);
* $db->runQuery("UPDATE table SET name = ? WHERE id = ?", [ $name, $id ]);
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array<int|string,mixed> $params - Ex: [ $something ]
*
* @return PDOStatement
*/
public function runQuery(string $sql, array $params = []): PDOStatement
{
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
$statement = $this->prepare($sql);
$statement->execute($params);
return $statement;
}
/**
* Pulls one field from the query
*
* Ex: $id = $db->fetchField("SELECT id FROM table WHERE something = ?", [ $something ]);
*
* @param string $sql - Ex: "SELECT id FROM table WHERE something = ?"
* @param array<int|string,mixed> $params - Ex: [ $something ]
*
* @return mixed
*/
public function fetchField(string $sql, array $params = [])
{
$result = $this->fetchRow($sql, $params);
$data = $result->getData();
return reset($data);
}
/**
* Pulls one row from the query
*
* Ex: $row = $db->fetchRow("SELECT * FROM table WHERE something = ?", [ $something ]);
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array<int|string,mixed> $params - Ex: [ $something ]
*
* @return Collection
*/
public function fetchRow(string $sql, array $params = []): Collection
{
$sql .= stripos($sql, 'LIMIT') === false ? ' LIMIT 1' : '';
$result = $this->fetchAll($sql, $params);
return count($result) > 0 ? $result[0] : new Collection();
}
/**
* Pulls all rows from the query
*
* Ex: $rows = $db->fetchAll("SELECT * FROM table WHERE something = ?", [ $something ]);
* foreach($rows as $row) {
* // ...
* }
*
* @param string $sql - Ex: "SELECT * FROM table WHERE something = ?"
* @param array<int|string,mixed> $params - Ex: [ $something ]
*
* @return array<int,Collection>
*/
public function fetchAll(string $sql, array $params = [])
{
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
$statement = $this->prepare($sql);
$statement->execute($params);
$results = $statement->fetchAll();
if (is_array($results) === true && count($results) > 0) {
foreach ($results as &$result) {
$result = new Collection($result);
}
} else {
$results = [];
}
return $results;
}
/**
* Don't worry about this guy. Converts stuff for IN statements
*
* Ex: $row = $db->fetchAll("SELECT * FROM table WHERE id = ? AND something IN(?), [ $id, [1,2,3] ]);
* Converts this to "SELECT * FROM table WHERE id = ? AND something IN(?,?,?)"
*
* @param string $sql the sql statement
* @param array<int|string,mixed> $params the params for the sql statement
*
* @return array<string,string|array<int|string,mixed>>
*/
protected function processInStatementSql(string $sql, array $params = []): array
{
// Replace "IN(?)" with "IN(?,?,?)"
$sql = preg_replace('/IN\s*\(\s*\?\s*\)/i', 'IN(?)', $sql);
$current_index = 0;
while (($current_index = strpos($sql, 'IN(?)', $current_index)) !== false) {
$preceeding_count = substr_count($sql, '?', 0, $current_index - 1);
$param = $params[$preceeding_count];
$question_marks = '?';
if (is_string($param) || is_array($param)) {
$params_to_use = $param;
if (is_string($param)) {
$params_to_use = explode(',', $param);
}
foreach ($params_to_use as $key => $value) {
if (is_string($value)) {
$params_to_use[$key] = trim($value);
}
}
$question_marks = join(',', array_fill(0, count($params_to_use), '?'));
$sql = substr_replace($sql, $question_marks, $current_index + 3, 1);
array_splice($params, $preceeding_count, 1, $params_to_use);
}
$current_index += strlen($question_marks) + 4;
}
return ['sql' => $sql, 'params' => $params];
}
}

417
api/flight/net/Request.php Normal file
View File

@@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace flight\net;
use flight\util\Collection;
/**
* The Request class represents an HTTP request. Data from
* all the super globals $_GET, $_POST, $_COOKIE, and $_FILES
* are stored and accessible via the Request object.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*
* The default request properties are:
*
* - **url** - The URL being requested
* - **base** - The parent subdirectory of the URL
* - **method** - The request method (GET, POST, PUT, DELETE)
* - **referrer** - The referrer URL
* - **ip** - IP address of the client
* - **ajax** - Whether the request is an AJAX request
* - **scheme** - The server protocol (http, https)
* - **user_agent** - Browser information
* - **type** - The content type
* - **length** - The content length
* - **query** - Query string parameters
* - **data** - Post parameters
* - **cookies** - Cookie parameters
* - **files** - Uploaded files
* - **secure** - Connection is secure
* - **accept** - HTTP accept parameters
* - **proxy_ip** - Proxy IP address of the client
*/
class Request
{
/**
* URL being requested
*/
public string $url;
/**
* Parent subdirectory of the URL
*/
public string $base;
/**
* Request method (GET, POST, PUT, DELETE)
*/
public string $method;
/**
* Referrer URL
*/
public string $referrer;
/**
* IP address of the client
*/
public string $ip;
/**
* Whether the request is an AJAX request
*/
public bool $ajax;
/**
* Server protocol (http, https)
*/
public string $scheme;
/**
* Browser information
*/
public string $user_agent;
/**
* Content type
*/
public string $type;
/**
* Content length
*/
public int $length;
/**
* Query string parameters
*/
public Collection $query;
/**
* Post parameters
*/
public Collection $data;
/**
* Cookie parameters
*/
public Collection $cookies;
/**
* Uploaded files
*/
public Collection $files;
/**
* Whether the connection is secure
*/
public bool $secure;
/**
* HTTP accept parameters
*/
public string $accept;
/**
* Proxy IP address of the client
*/
public string $proxy_ip;
/**
* HTTP host name
*/
public string $host;
/**
* Stream path for where to pull the request body from
*/
private string $stream_path = 'php://input';
/**
* Raw HTTP request body
*/
public string $body = '';
/**
* Constructor.
*
* @param array<string, mixed> $config Request configuration
*/
public function __construct(array $config = [])
{
// Default properties
if (empty($config)) {
$config = [
'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')),
'base' => str_replace(['\\', ' '], ['/', '%20'], \dirname(self::getVar('SCRIPT_NAME'))),
'method' => self::getMethod(),
'referrer' => self::getVar('HTTP_REFERER'),
'ip' => self::getVar('REMOTE_ADDR'),
'ajax' => self::getVar('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest',
'scheme' => self::getScheme(),
'user_agent' => self::getVar('HTTP_USER_AGENT'),
'type' => self::getVar('CONTENT_TYPE'),
'length' => intval(self::getVar('CONTENT_LENGTH', 0)),
'query' => new Collection($_GET),
'data' => new Collection($_POST),
'cookies' => new Collection($_COOKIE),
'files' => new Collection($_FILES),
'secure' => self::getScheme() === 'https',
'accept' => self::getVar('HTTP_ACCEPT'),
'proxy_ip' => self::getProxyIpAddress(),
'host' => self::getVar('HTTP_HOST'),
];
}
$this->init($config);
}
/**
* Initialize request properties.
*
* @param array<string, mixed> $properties Array of request properties
*
* @return self
*/
public function init(array $properties = []): self
{
// Set all the defined properties
foreach ($properties as $name => $value) {
$this->{$name} = $value;
}
// Get the requested URL without the base directory
// This rewrites the url in case the public url and base directories match
// (such as installing on a subdirectory in a web server)
// @see testInitUrlSameAsBaseDirectory
if ($this->base !== '/' && $this->base !== '' && strpos($this->url, $this->base) === 0) {
$this->url = substr($this->url, \strlen($this->base));
}
// Default url
if (empty($this->url) === true) {
$this->url = '/';
} else {
// Merge URL query parameters with $_GET
$_GET = array_merge($_GET, self::parseQuery($this->url));
$this->query->setData($_GET);
}
// Check for JSON input
if (strpos($this->type, 'application/json') === 0) {
$body = $this->getBody();
if ($body !== '') {
$data = json_decode($body, true);
if (is_array($data) === true) {
$this->data->setData($data);
}
}
}
return $this;
}
/**
* Gets the body of the request.
*
* @return string Raw HTTP request body
*/
public function getBody(): string
{
$body = $this->body;
if ($body !== '') {
return $body;
}
$method = $this->method ?? self::getMethod();
if ($method === 'POST' || $method === 'PUT' || $method === 'DELETE' || $method === 'PATCH') {
$body = file_get_contents($this->stream_path);
}
$this->body = $body;
return $body;
}
/**
* Gets the request method.
*/
public static function getMethod(): string
{
$method = self::getVar('REQUEST_METHOD', 'GET');
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) === true) {
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
} elseif (isset($_REQUEST['_method']) === true) {
$method = $_REQUEST['_method'];
}
return strtoupper($method);
}
/**
* Gets the real remote IP address.
*
* @return string IP address
*/
public static function getProxyIpAddress(): string
{
$forwarded = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
];
$flags = \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE;
foreach ($forwarded as $key) {
if (\array_key_exists($key, $_SERVER) === true) {
sscanf($_SERVER[$key], '%[^,]', $ip);
if (filter_var($ip, \FILTER_VALIDATE_IP, $flags) !== false) {
return $ip;
}
}
}
return '';
}
/**
* Gets a variable from $_SERVER using $default if not provided.
*
* @param string $var Variable name
* @param mixed $default Default value to substitute
*
* @return mixed Server variable value
*/
public static function getVar(string $var, $default = '')
{
return $_SERVER[$var] ?? $default;
}
/**
* This will pull a header from the request.
*
* @param string $header Header name. Can be caps, lowercase, or mixed.
* @param string $default Default value if the header does not exist
*
* @return string
*/
public static function getHeader(string $header, $default = ''): string
{
$header = 'HTTP_' . strtoupper(str_replace('-', '_', $header));
return self::getVar($header, $default);
}
/**
* Gets all the request headers
*
* @return array<string, string|int>
*/
public static function getHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
// converts headers like HTTP_CUSTOM_HEADER to Custom-Header
$key = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
$headers[$key] = $value;
}
}
return $headers;
}
/**
* Alias of Request->getHeader(). Gets a single header.
*
* @param string $header Header name. Can be caps, lowercase, or mixed.
* @param string $default Default value if the header does not exist
*
* @return string
*/
public static function header(string $header, $default = '')
{
return self::getHeader($header, $default);
}
/**
* Alias of Request->getHeaders(). Gets all the request headers
*
* @return array<string, string|int>
*/
public static function headers(): array
{
return self::getHeaders();
}
/**
* Gets the full request URL.
*
* @return string URL
*/
public function getFullUrl(): string
{
return $this->scheme . '://' . $this->host . $this->url;
}
/**
* Grabs the scheme and host. Does not end with a /
*
* @return string
*/
public function getBaseUrl(): string
{
return $this->scheme . '://' . $this->host;
}
/**
* Parse query parameters from a URL.
*
* @param string $url URL string
*
* @return array<string, int|string|array<int|string, int|string>>
*/
public static function parseQuery(string $url): array
{
$params = [];
$args = parse_url($url);
if (isset($args['query']) === true) {
parse_str($args['query'], $params);
}
return $params;
}
/**
* Gets the URL Scheme
*
* @return string 'http'|'https'
*/
public static function getScheme(): string
{
if (
(isset($_SERVER['HTTPS']) === true && strtolower($_SERVER['HTTPS']) === 'on')
||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) === true && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
||
(isset($_SERVER['HTTP_FRONT_END_HTTPS']) === true && $_SERVER['HTTP_FRONT_END_HTTPS'] === 'on')
||
(isset($_SERVER['REQUEST_SCHEME']) === true && $_SERVER['REQUEST_SCHEME'] === 'https')
) {
return 'https';
}
return 'http';
}
}

473
api/flight/net/Response.php Normal file
View File

@@ -0,0 +1,473 @@
<?php
declare(strict_types=1);
namespace flight\net;
use Exception;
/**
* The Response class represents an HTTP response. The object
* contains the response headers, HTTP status code, and response
* body.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Response
{
/**
* Content-Length header.
*/
public bool $content_length = true;
/**
* This is to maintain legacy handling of output buffering
* which causes a lot of problems. This will be removed
* in v4
*
* @var boolean
*/
public bool $v2_output_buffering = false;
/**
* HTTP status codes
*
* @var array<int, ?string> $codes
*/
public static array $codes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
];
/**
* HTTP status
*/
protected int $status = 200;
/**
* HTTP response headers
*
* @var array<string,int|string|array<int,string>> $headers
*/
protected array $headers = [];
/**
* HTTP response body
*/
protected string $body = '';
/**
* HTTP response sent
*/
protected bool $sent = false;
/**
* These are callbacks that can process the response body before it's sent
*
* @var array<int, callable> $responseBodyCallbacks
*/
protected array $responseBodyCallbacks = [];
/**
* Sets the HTTP status of the response.
*
* @param ?int $code HTTP status code.
*
* @throws Exception If invalid status code
*
* @return int|$this Self reference
*/
public function status(?int $code = null)
{
if ($code === null) {
return $this->status;
}
if (\array_key_exists($code, self::$codes)) {
$this->status = $code;
} else {
throw new Exception('Invalid status code.');
}
return $this;
}
/**
* Adds a header to the response.
*
* @param array<string, int|string>|string $name Header name or array of names and values
* @param ?string $value Header value
*
* @return $this
*/
public function header($name, ?string $value = null): self
{
if (\is_array($name)) {
foreach ($name as $k => $v) {
$this->headers[$k] = $v;
}
} else {
$this->headers[$name] = $value;
}
return $this;
}
/**
* Gets a single header from the response.
*
* @param string $name the name of the header
*
* @return string|null
*/
public function getHeader(string $name): ?string
{
$headers = $this->headers;
// lowercase all the header keys
$headers = array_change_key_case($headers, CASE_LOWER);
return $headers[strtolower($name)] ?? null;
}
/**
* Alias of Response->header(). Adds a header to the response.
*
* @param array<string, int|string>|string $name Header name or array of names and values
* @param ?string $value Header value
*
* @return $this
*/
public function setHeader($name, ?string $value): self
{
return $this->header($name, $value);
}
/**
* Returns the headers from the response.
*
* @return array<string, int|string|array<int, string>>
*/
public function headers(): array
{
return $this->headers;
}
/**
* Alias for Response->headers(). Returns the headers from the response.
*
* @return array<string, int|string|array<int, string>>
*/
public function getHeaders(): array
{
return $this->headers();
}
/**
* Writes content to the response body.
*
* @param string $str Response content
* @param bool $overwrite Overwrite the response body
*
* @return $this Self reference
*/
public function write(string $str, bool $overwrite = false): self
{
if ($overwrite === true) {
$this->clearBody();
}
$this->body .= $str;
return $this;
}
/**
* Clears the response body.
*
* @return $this Self reference
*/
public function clearBody(): self
{
$this->body = '';
return $this;
}
/**
* Clears the response.
*
* @return $this Self reference
*/
public function clear(): self
{
$this->status = 200;
$this->headers = [];
$this->clearBody();
// This needs to clear the output buffer if it's on
if ($this->v2_output_buffering === false && ob_get_length() > 0) {
ob_clean();
}
return $this;
}
/**
* Sets caching headers for the response.
*
* @param int|string|false $expires Expiration time as time() or as strtotime() string value
*
* @return $this Self reference
*/
public function cache($expires): self
{
if ($expires === false) {
$this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
$this->headers['Cache-Control'] = [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
];
$this->headers['Pragma'] = 'no-cache';
} else {
$expires = \is_int($expires) ? $expires : strtotime($expires);
$this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT';
$this->headers['Cache-Control'] = 'max-age=' . ($expires - time());
if (isset($this->headers['Pragma']) && $this->headers['Pragma'] === 'no-cache') {
unset($this->headers['Pragma']);
}
}
return $this;
}
/**
* Sends HTTP headers.
*
* @return $this Self reference
*/
public function sendHeaders(): self
{
// Send status code header
if (strpos(\PHP_SAPI, 'cgi') !== false) {
// @codeCoverageIgnoreStart
$this->setRealHeader(
sprintf(
'Status: %d %s',
$this->status,
self::$codes[$this->status]
),
true
);
// @codeCoverageIgnoreEnd
} else {
$this->setRealHeader(
sprintf(
'%s %d %s',
$_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
$this->status,
self::$codes[$this->status]
),
true,
$this->status
);
}
if ($this->content_length === true) {
// Send content length
$length = $this->getContentLength();
if ($length > 0) {
$this->setHeader('Content-Length', (string) $length);
}
}
// Send other headers
foreach ($this->headers as $field => $value) {
if (\is_array($value)) {
foreach ($value as $v) {
$this->setRealHeader($field . ': ' . $v, false);
}
} else {
$this->setRealHeader($field . ': ' . $value);
}
}
return $this;
}
/**
* Sets a real header. Mostly used for test mocking.
*
* @param string $header_string The header string you would pass to header()
* @param bool $replace The optional replace parameter indicates whether the
* header should replace a previous similar header, or add a second header of
* the same type. By default it will replace, but if you pass in false as the
* second argument you can force multiple headers of the same type.
* @param int $response_code The response code to send
*
* @return self
*
* @codeCoverageIgnore
*/
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self
{
header($header_string, $replace, $response_code);
return $this;
}
/**
* Gets the content length.
*/
public function getContentLength(): int
{
return \extension_loaded('mbstring') ?
mb_strlen($this->body, 'latin1') :
\strlen($this->body);
}
/**
* Gets the response body
*
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* Gets whether response body was sent.
*/
public function sent(): bool
{
return $this->sent;
}
/**
* Marks the response as sent.
*/
public function markAsSent(): void
{
$this->sent = true;
}
/**
* Sends a HTTP response.
*/
public function send(): void
{
// legacy way of handling this
if ($this->v2_output_buffering === true) {
if (ob_get_length() > 0) {
ob_end_clean(); // @codeCoverageIgnore
}
}
// Only for the v3 output buffering.
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
}
if (headers_sent() === false) {
$this->sendHeaders(); // @codeCoverageIgnore
}
echo $this->body;
$this->sent = true;
}
/**
* Adds a callback to process the response body before it's sent. These are processed in the order
* they are added
*
* @param callable $callback The callback to process the response body
*
* @return void
*/
public function addResponseBodyCallback(callable $callback): void
{
$this->responseBodyCallbacks[] = $callback;
}
/**
* Cycles through the response body callbacks and processes them in order
*
* @return void
*/
protected function processResponseCallbacks(): void
{
foreach ($this->responseBodyCallbacks as $callback) {
$this->body = $callback($this->body);
}
}
}

266
api/flight/net/Route.php Normal file
View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace flight\net;
/**
* The Route class is responsible for routing an HTTP request to
* an assigned callback function. The Router tries to match the
* requested URL against a series of URL patterns.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Route
{
/**
* URL pattern
*/
public string $pattern;
/**
* Callback function
*
* @var mixed
*/
public $callback;
/**
* HTTP methods
*
* @var array<int, string>
*/
public array $methods = [];
/**
* Route parameters
*
* @var array<int, ?string>
*/
public array $params = [];
/**
* Matching regular expression
*/
public ?string $regex = null;
/**
* URL splat content
*/
public string $splat = '';
/**
* Pass self in callback parameters
*/
public bool $pass = false;
/**
* The alias is a way to identify the route using a simple name ex: 'login' instead of /admin/login
*/
public string $alias = '';
/**
* The middleware to be applied to the route
*
* @var array<int, callable|object|string>
*/
public array $middleware = [];
/** Whether the response for this route should be streamed. */
public bool $is_streamed = false;
/**
* If this route is streamed, the headers to be sent before the response.
*
* @var array<string, mixed>
*/
public array $streamed_headers = [];
/**
* Constructor.
*
* @param string $pattern URL pattern
* @param callable|string $callback Callback function
* @param array<int, string> $methods HTTP methods
* @param bool $pass Pass self in callback parameters
*/
public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '')
{
$this->pattern = $pattern;
$this->callback = $callback;
$this->methods = $methods;
$this->pass = $pass;
$this->alias = $alias;
}
/**
* Checks if a URL matches the route pattern. Also parses named parameters in the URL.
*
* @param string $url Requested URL (original format, not URL decoded)
* @param bool $case_sensitive Case sensitive matching
*
* @return bool Match status
*/
public function matchUrl(string $url, bool $case_sensitive = false): bool
{
// Wildcard or exact match
if ($this->pattern === '*' || $this->pattern === $url) {
return true;
}
$ids = [];
$last_char = substr($this->pattern, -1);
// Get splat
if ($last_char === '*') {
$n = 0;
$len = \strlen($url);
$count = substr_count($this->pattern, '/');
for ($i = 0; $i < $len; $i++) {
if ($url[$i] === '/') {
++$n;
}
if ($n === $count) {
break;
}
}
$this->splat = urldecode(strval(substr($url, $i + 1)));
}
// Build the regex for matching
$pattern_utf_chars_encoded = preg_replace_callback(
'#(\\p{L}+)#u',
static function ($matches) {
return urlencode($matches[0]);
},
$this->pattern
);
$regex = str_replace([')', '/*'], [')?', '(/?|/.*?)'], $pattern_utf_chars_encoded);
$regex = preg_replace_callback(
'#@([\w]+)(:([^/\(\)]*))?#',
static function ($matches) use (&$ids) {
$ids[$matches[1]] = null;
if (isset($matches[3])) {
return '(?P<' . $matches[1] . '>' . $matches[3] . ')';
}
return '(?P<' . $matches[1] . '>[^/\?]+)';
},
$regex
);
$regex .= $last_char === '/' ? '?' : '/?';
// Attempt to match route and named parameters
if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) {
return false;
}
foreach (array_keys($ids) as $k) {
$this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null;
}
$this->regex = $regex;
return true;
}
/**
* Checks if an HTTP method matches the route methods.
*
* @param string $method HTTP method
*
* @return bool Match status
*/
public function matchMethod(string $method): bool
{
return \count(array_intersect([$method, '*'], $this->methods)) > 0;
}
/**
* Checks if an alias matches the route alias.
*/
public function matchAlias(string $alias): bool
{
return $this->alias === $alias;
}
/**
* Hydrates the route url with the given parameters
*
* @param array<string, mixed> $params the parameters to pass to the route
*/
public function hydrateUrl(array $params = []): string
{
$url = preg_replace_callback("/(?:@([\w]+)(?:\:([^\/]+))?\)*)/i", function ($match) use ($params) {
if (isset($match[1]) && isset($params[$match[1]])) {
return $params[$match[1]];
}
}, $this->pattern);
// catches potential optional parameter
$url = str_replace('(/', '/', $url);
// trim any trailing slashes
if ($url !== '/') {
$url = rtrim($url, '/');
}
return $url;
}
/**
* Sets the route alias
*
* @return $this
*/
public function setAlias(string $alias): self
{
$this->alias = $alias;
return $this;
}
/**
* Sets the route middleware
*
* @param array<int, callable|string>|callable|string $middleware
*/
public function addMiddleware($middleware): self
{
if (is_array($middleware) === true) {
$this->middleware = array_merge($this->middleware, $middleware);
} else {
$this->middleware[] = $middleware;
}
return $this;
}
/**
* If the response should be streamed
*
* @return self
*/
public function stream(): self
{
$this->is_streamed = true;
return $this;
}
/**
* This will allow the response for this route to be streamed.
*
* @param array<string, mixed> $headers a key value of headers to set before the stream starts.
*
* @return $this
*/
public function streamWithHeaders(array $headers): self
{
$this->is_streamed = true;
$this->streamed_headers = $headers;
return $this;
}
}

330
api/flight/net/Router.php Normal file
View File

@@ -0,0 +1,330 @@
<?php
declare(strict_types=1);
namespace flight\net;
use Exception;
use flight\net\Route;
/**
* The Router class is responsible for routing an HTTP request to
* an assigned callback function. The Router tries to match the
* requested URL against a series of URL patterns.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Router
{
/**
* Case sensitive matching.
*/
public bool $case_sensitive = false;
/**
* Mapped routes.
*
* @var array<int,Route> $routes
*/
protected array $routes = [];
/**
* The current route that is has been found and executed.
*/
public ?Route $executedRoute = null;
/**
* Pointer to current route.
*/
protected int $index = 0;
/**
* When groups are used, this is mapped against all the routes
*/
protected string $groupPrefix = '';
/**
* Group Middleware
*
* @var array<int,mixed>
*/
protected array $groupMiddlewares = [];
/**
* Allowed HTTP methods
*
* @var array<int, string>
*/
protected array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
/**
* Gets mapped routes.
*
* @return array<int,Route> Array of routes
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* Clears all routes in the router.
*/
public function clear(): void
{
$this->routes = [];
}
/**
* Maps a URL pattern to a callback function.
*
* @param string $pattern URL pattern to match.
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback.
* @param string $route_alias Alias for the route.
*/
public function map(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{
// This means that the route ies defined in a group, but the defined route is the base
// url path. Note the '' in route()
// Ex: Flight::group('/api', function() {
// Flight::route('', function() {});
// }
// Keep the space so that it can execute the below code normally
if ($this->groupPrefix !== '') {
$url = ltrim($pattern);
} else {
$url = trim($pattern);
}
$methods = ['*'];
if (strpos($url, ' ') !== false) {
[$method, $url] = explode(' ', $url, 2);
$url = trim($url);
$methods = explode('|', $method);
// Add head requests to get methods, should they come in as a get request
if (in_array('GET', $methods, true) === true && in_array('HEAD', $methods, true) === false) {
$methods[] = 'HEAD';
}
}
// And this finishes it off.
if ($this->groupPrefix !== '') {
$url = rtrim($this->groupPrefix . $url);
}
$route = new Route($url, $callback, $methods, $pass_route, $route_alias);
// to handle group middleware
foreach ($this->groupMiddlewares as $gm) {
$route->addMiddleware($gm);
}
$this->routes[] = $route;
return $route;
}
/**
* Creates a GET based route
*
* @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias Alias for the route
*/
public function get(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('GET ' . $pattern, $callback, $pass_route, $alias);
}
/**
* Creates a POST based route
*
* @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias Alias for the route
*/
public function post(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('POST ' . $pattern, $callback, $pass_route, $alias);
}
/**
* Creates a PUT based route
*
* @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias Alias for the route
*/
public function put(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias);
}
/**
* Creates a PATCH based route
*
* @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias Alias for the route
*/
public function patch(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias);
}
/**
* Creates a DELETE based route
*
* @param string $pattern URL pattern to match
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias Alias for the route
*/
public function delete(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias);
}
/**
* Group together a set of routes
*
* @param string $groupPrefix group URL prefix (such as /api/v1)
* @param callable $callback The necessary calling that holds the Router class
* @param array<int, callable|object> $groupMiddlewares
* The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]`
*/
public function group(string $groupPrefix, callable $callback, array $groupMiddlewares = []): void
{
$oldGroupPrefix = $this->groupPrefix;
$oldGroupMiddlewares = $this->groupMiddlewares;
$this->groupPrefix .= $groupPrefix;
$this->groupMiddlewares = array_merge($this->groupMiddlewares, $groupMiddlewares);
$callback($this);
$this->groupPrefix = $oldGroupPrefix;
$this->groupMiddlewares = $oldGroupMiddlewares;
}
/**
* Routes the current request.
*
* @return false|Route Matching route or false if no match
*/
public function route(Request $request)
{
while ($route = $this->current()) {
$urlMatches = $route->matchUrl($request->url, $this->case_sensitive);
$methodMatches = $route->matchMethod($request->method);
if ($urlMatches === true && $methodMatches === true) {
$this->executedRoute = $route;
return $route;
// capture the route but don't execute it. We'll use this in Engine->start() to throw a 405
} elseif ($urlMatches === true && $methodMatches === false) {
$this->executedRoute = $route;
}
$this->next();
}
return false;
}
/**
* Gets the URL for a given route alias
*
* @param string $alias the alias to match
* @param array<string,mixed> $params the parameters to pass to the route
*/
public function getUrlByAlias(string $alias, array $params = []): string
{
$potential_aliases = [];
foreach ($this->routes as $route) {
$potential_aliases[] = $route->alias;
if ($route->matchAlias($alias)) {
// This will make it so the params that already
// exist in the url will be passed in.
if (!empty($this->executedRoute->params)) {
$params = $params + $this->executedRoute->params;
}
return $route->hydrateUrl($params);
}
}
// use a levenshtein to find the closest match and make a recommendation
$closest_match = '';
$closest_match_distance = 0;
foreach ($potential_aliases as $potential_alias) {
$levenshtein_distance = levenshtein($alias, $potential_alias);
if ($levenshtein_distance > $closest_match_distance) {
$closest_match = $potential_alias;
$closest_match_distance = $levenshtein_distance;
}
}
$exception_message = 'No route found with alias: \'' . $alias . '\'.';
if ($closest_match !== '') {
$exception_message .= ' Did you mean \'' . $closest_match . '\'?';
}
throw new Exception($exception_message);
}
/**
* Rewinds the current route index.
*/
public function rewind(): void
{
$this->index = 0;
}
/**
* Checks if more routes can be iterated.
*
* @return bool More routes
*/
public function valid(): bool
{
return isset($this->routes[$this->index]);
}
/**
* Gets the current route.
*
* @return false|Route
*/
public function current()
{
return $this->routes[$this->index] ?? false;
}
/**
* Gets the previous route.
*/
public function previous(): void
{
--$this->index;
}
/**
* Gets the next route.
*/
public function next(): void
{
++$this->index;
}
/**
* Reset to the first route.
*/
public function reset(): void
{
$this->rewind();
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace flight\template;
/**
* The View class represents output to be displayed. It provides
* methods for managing view data and inserts the data into
* view templates upon rendering.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class View
{
/** Location of view templates. */
public string $path;
/** File extension. */
public string $extension = '.php';
public bool $preserveVars = true;
/**
* View variables.
*
* @var array<string, mixed> $vars
*/
protected array $vars = [];
/** Template file. */
private string $template;
/**
* Constructor.
*
* @param string $path Path to templates directory
*/
public function __construct(string $path = '.')
{
$this->path = $path;
}
/**
* Gets a template variable.
*
* @return mixed Variable value or `null` if doesn't exists
*/
public function get(string $key)
{
return $this->vars[$key] ?? null;
}
/**
* Sets a template variable.
*
* @param string|iterable<string, mixed> $key
* @param mixed $value Value
*
* @return self
*/
public function set($key, $value = null): self
{
if (\is_iterable($key)) {
foreach ($key as $k => $v) {
$this->vars[$k] = $v;
}
} else {
$this->vars[$key] = $value;
}
return $this;
}
/**
* Checks if a template variable is set.
*
* @return bool If key exists
*/
public function has(string $key): bool
{
return isset($this->vars[$key]);
}
/**
* Unsets a template variable. If no key is passed in, clear all variables.
*
* @return $this
*/
public function clear(?string $key = null): self
{
if ($key === null) {
$this->vars = [];
} else {
unset($this->vars[$key]);
}
return $this;
}
/**
* Renders a template.
*
* @param string $file Template file
* @param ?array<string, mixed> $data Template data
*
* @throws \Exception If template not found
*/
public function render(string $file, ?array $data = null): void
{
$this->template = $this->getTemplate($file);
if (!\file_exists($this->template)) {
$normalized_path = self::normalizePath($this->template);
throw new \Exception("Template file not found: {$normalized_path}.");
}
\extract($this->vars);
if (\is_array($data) === true) {
\extract($data);
if ($this->preserveVars === true) {
$this->vars = \array_merge($this->vars, $data);
}
}
include $this->template;
}
/**
* Gets the output of a template.
*
* @param string $file Template file
* @param ?array<string, mixed> $data Template data
*
* @return string Output of template
*/
public function fetch(string $file, ?array $data = null): string
{
\ob_start();
$this->render($file, $data);
return \ob_get_clean();
}
/**
* Checks if a template file exists.
*
* @param string $file Template file
*
* @return bool Template file exists
*/
public function exists(string $file): bool
{
return \file_exists($this->getTemplate($file));
}
/**
* Gets the full path to a template file.
*
* @param string $file Template file
*
* @return string Template file location
*/
public function getTemplate(string $file): string
{
$ext = $this->extension;
if (!empty($ext) && (\substr($file, -1 * \strlen($ext)) != $ext)) {
$file .= $ext;
}
$is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN';
if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) {
return $file;
}
return $this->path . DIRECTORY_SEPARATOR . $file;
}
/**
* Displays escaped output.
*
* @param string $str String to escape
*
* @return string Escaped string
*/
public function e(string $str): string
{
$value = \htmlentities($str);
echo $value;
return $value;
}
protected static function normalizePath(string $path, string $separator = DIRECTORY_SEPARATOR): string
{
return \str_replace(['\\', '/'], $separator, $path);
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace flight\util;
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* The Collection class allows you to access a set of data
* using both array and object notation.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @implements ArrayAccess<string, mixed>
* @implements Iterator<string, mixed>
*/
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
{
/**
* Collection data.
*
* @var array<string, mixed>
*/
private array $data;
/**
* Constructor.
*
* @param array<string, mixed> $data Initial data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Gets an item.
*
* @return mixed Value if `$key` exists in collection data, otherwise returns `NULL`
*/
public function __get(string $key)
{
return $this->data[$key] ?? null;
}
/**
* Set an item.
*
* @param mixed $value Value
*/
public function __set(string $key, $value): void
{
$this->data[$key] = $value;
}
/**
* Checks if an item exists.
*/
public function __isset(string $key): bool
{
return isset($this->data[$key]);
}
/**
* Removes an item.
*/
public function __unset(string $key): void
{
unset($this->data[$key]);
}
/**
* Gets an item at the offset.
*
* @param string $offset Offset
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->data[$offset] ?? null;
}
/**
* Sets an item at the offset.
*
* @param ?string $offset Offset
* @param mixed $value Value
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value): void
{
if ($offset === null) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
/**
* Checks if an item exists at the offset.
*
* @param string $offset
*/
public function offsetExists($offset): bool
{
return isset($this->data[$offset]);
}
/**
* Removes an item at the offset.
*
* @param string $offset
*/
public function offsetUnset($offset): void
{
unset($this->data[$offset]);
}
/**
* Resets the collection.
*/
public function rewind(): void
{
reset($this->data);
}
/**
* Gets current collection item.
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->data);
}
/**
* Gets current collection key.
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->data);
}
/**
* Gets the next collection value.
*/
#[\ReturnTypeWillChange]
public function next(): void
{
next($this->data);
}
/**
* Checks if the current collection key is valid.
*/
public function valid(): bool
{
return key($this->data) !== null;
}
/**
* Gets the size of the collection.
*/
public function count(): int
{
return \count($this->data);
}
/**
* Gets the item keys.
*
* @return array<int, string> Collection keys
*/
public function keys(): array
{
return array_keys($this->data);
}
/**
* Gets the collection data.
*
* @return array<string, mixed> Collection data
*/
public function getData(): array
{
return $this->data;
}
/**
* Sets the collection data.
*
* @param array<string, mixed> $data New collection data
*/
public function setData(array $data): void
{
$this->data = $data;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->data;
}
/**
* Removes all items from the collection.
*/
public function clear(): void
{
$this->data = [];
}
}

View File

@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// This file is only here so that the PHP8 attribute for doesn't throw an error in files
class ReturnTypeWillChange
{
}