pull/689/merge
fadrian06 11 hours ago committed by GitHub
commit f461023690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,9 +5,8 @@ indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
indent_size = 2
[tests/views/*.php]
insert_final_newline = false

@ -16,12 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2`
- Lint code: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1)
- Check code style: `composer phpcs`
- Format code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Test coverage: `composer test-coverage`
## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS)
- Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

4
.gitattributes vendored

@ -5,9 +5,9 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/CONTRIBUTING.md export-ignore
/index.php export-ignore
/phpcs.xml.dist export-ignore
/phpstan-baseline.neon export-ignore
/phpstan.dist.neon export-ignore
/phpunit-watcher.yml export-ignore
/phpunit-watcher.yml.dist export-ignore
/phpunit.xml.dist export-ignore
/README.md export-ignore

@ -16,12 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2`
- Lint code: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1)
- Check code style: `composer phpcs`
- Format code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Test coverage: `composer test-coverage`
## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS)
- Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

16
.gitignore vendored

@ -1,15 +1,9 @@
.idea/
.vscode/
vendor/
composer.phar
composer.lock
.phpunit.result.cache
coverage/
*.sublime*
clover.xml
.phpunit.result.cache
/coverage/
/vendor/
composer.lock
phpcs.xml
phpstan.neon
phpunit-watcher.yml
phpunit.xml
.runway-config.json
.runway-creds.json
.DS_Store

@ -11,7 +11,7 @@ Flight aims to be simple and fast. Anything that compromises either of those two
* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are:
* PHPStan is at level 6.
* `===` instead of truthy or falsey statements like `==` or `!is_array()`.
* `===` instead of truthy or falsey statements like `==`.
* **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4.

@ -1,19 +1,19 @@
{
"name": "flightphp/core",
"description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications. This is the maintained fork of mikecao/flight",
"homepage": "http://flightphp.com",
"homepage": "https://docs.flightphp.com/",
"license": "MIT",
"authors": [
{
"name": "Mike Cao",
"email": "mike@mikecao.com",
"homepage": "http://www.mikecao.com/",
"homepage": "https://mikecao.com/",
"role": "Original Developer"
},
{
"name": "Franyer Sánchez",
"email": "franyeradriansanchez@gmail.com",
"homepage": "https://faslatam.42web.io",
"homepage": "https://faslatam.42web.io/",
"role": "Maintainer"
},
{
@ -27,21 +27,16 @@
"ext-json": "*"
},
"autoload": {
"files": [
"flight/autoload.php"
]
},
"autoload-dev": {
"classmap": [
"tests/classes/"
"src/Flight.php"
],
"psr-4": {
"Tests\\PHP8\\": [
"tests/named-arguments"
],
"Tests\\Server\\": "tests/server",
"Tests\\ServerV2\\": "tests/server-v2",
"tests\\groupcompactsyntax\\": "tests/groupcompactsyntax"
"flight\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"tests\\": "tests"
}
},
"require-dev": {
@ -54,7 +49,9 @@
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.6",
"rregeer/phpunit-coverage-check": "^0.3.1",
"squizlabs/php_codesniffer": "^4.0"
"spatie/phpunit-watcher": "^1.23",
"squizlabs/php_codesniffer": "^4.0",
"symfony/var-dumper": "^5.4"
},
"config": {
"allow-plugins": {
@ -65,15 +62,12 @@
},
"scripts": {
"test": "phpunit",
"test-watcher": [
"phpunit-watcher || composer global require spatie/phpunit-watcher --dev",
"phpunit-watcher watch"
],
"test-watcher": "phpunit-watcher watch",
"test-coverage": [
"rm -f clover.xml",
"rm -rf coverage",
"@putenv XDEBUG_MODE=coverage",
"phpunit --coverage-html=coverage --coverage-clover=clover.xml",
"coverage-check clover.xml 100"
"phpunit --coverage-html coverage --coverage-clover coverage/clover.xml",
"coverage-check coverage/clover.xml 100"
],
"test-server": [
"echo \"Running Test Server\"",
@ -81,12 +75,7 @@
],
"test-server-v2": [
"echo \"Running Test Server\"",
"@php -S localhost:8000 -t tests/server-v2"
],
"test-coverage:win": [
"del clover.xml",
"phpunit --coverage-html=coverage --coverage-clover=clover.xml",
"coverage-check clover.xml 100"
"@php -S localhost:8000 -t tests/server_v2"
],
"test-performance": [
"echo \"Running Performance Tests...\"",
@ -97,13 +86,18 @@
"rm server.pid",
"echo \"Performance Tests Completed.\""
],
"lint": "phpstan --no-progress --memory-limit=256M",
"beautify": "phpcbf",
"phpcs": "phpcs",
"lint": [
"phpstan --no-progress --memory-limit=256M",
"phpcs"
],
"format": [
"phpcbf"
],
"post-install-cmd": [
"@php -r \"if (!file_exists('phpcs.xml')) copy('phpcs.xml.dist', 'phpcs.xml');\"",
"@php -r \"if (!file_exists('phpstan.neon')) copy('phpstan.dist.neon', 'phpstan.neon');\"",
"@php -r \"if (!file_exists('phpunit.xml')) copy('phpunit.xml.dist', 'phpunit.xml');\""
"@php -r \"if (!file_exists('phpunit.xml')) copy('phpunit.xml.dist', 'phpunit.xml');\"",
"@php -r \"if (!file_exists('phpunit-watcher.yml')) copy('phpunit-watcher.yml.dist', 'phpunit-watcher.yml');\""
]
},
"suggest": {

@ -1,145 +0,0 @@
<?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;
use flight\core\EventDispatcher;
use Psr\Container\ContainerInterface;
require_once __DIR__ . '/autoload.php';
/**
* The Flight class is a static representation of the framework.
*
* @license MIT, https://docs.flightphp.com/license
* @copyright Copyright (c) 2011-2025, Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com>
*
* @method static void start()
* @method static void path(string $dir)
* @method static void stop(int $code = null)
* @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true)
* @method static void register(string $name, string $class, array $params = [], callable $callback = null)
* @method static void unregister(string $methodName)
* @method static void registerContainerHandler($containerHandler)
* @method static EventDispatcher eventDispatcher()
* @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method static void group(string $pattern, callable $callback, array $group_middlewares = [])
* @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method static void resource(string $pattern, string $controllerClass, array $methods = [])
* @method static Router router()
* @method static string getUrl(string $alias, array $params = [])
* @method static void map(string $name, callable $callback)
* @method static void before(string $name, Closure $callback)
* @method static void after(string $name, Closure $callback)
* @method static void set($key, $value)
* @method static mixed get($key = null)
* @method static bool has(string $key)
* @method static void clear($key = null)
* @method static void render(string $file, array $data = null, string $key = null)
* @method static View view()
* @method void onEvent(string $event, callable $callback)
* @method void triggerEvent(string $event, ...$args)
* @method static Request request()
* @method static Response response()
* @method static void redirect(string $url, int $code = 303)
* @method static void json($data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @method static void jsonHalt($data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @method static void jsonp($data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @method static void error(Throwable $exception)
* @method static void notFound()
* @method static void methodNotFound(Route $route)
* @method static void etag(string $id, string $type = 'strong')
* @method static void lastModified(int $time)
* @method static void download(string $filePath)
*
* @phpstan-template FlightTemplate of object
* @phpstan-method static void register(string $name, class-string<FlightTemplate> $class, array<int|string, mixed> $params = [], (callable(class-string<FlightTemplate> $class, array<int|string, mixed> $params): void)|null $callback = null)
* @phpstan-method static void registerContainerHandler(ContainerInterface|callable(class-string<FlightTemplate> $id, array<int|string, mixed> $params): ?FlightTemplate $containerHandler)
* @phpstan-method static Route route(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method static void group(string $pattern, callable $callback, (class-string|callable|array{0: class-string, 1: string})[] $group_middlewares = [])
* @phpstan-method static Route post(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method static Route put(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method static Route patch(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method static Route delete(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method static void resource(string $pattern, class-string $controllerClass, array<string, string|array<string>> $methods = [])
* @phpstan-method static string getUrl(string $alias, array<string, mixed> $params = [])
* @phpstan-method static void before(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* @phpstan-method static void after(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* @phpstan-method static void set(string|iterable<string, mixed> $key, mixed $value)
* @phpstan-method static mixed get(?string $key)
* @phpstan-method static void render(string $file, ?array<string, mixed> $data = null, ?string $key = null)
* @phpstan-method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @phpstan-method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @phpstan-method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
*
* Note: IDEs will use standard @method tags for autocompletion,
* while PHPStan will use @phpstan-* tags for advanced type checking.
*/
// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace
class Flight
{
/**
* @var Engine<FlightTemplate>
*/
private static Engine $engine;
/**
* 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<FlightTemplate> Application instance */
public static function app(): Engine
{
return self::$engine ?? self::$engine = new Engine();
}
/**
* Set the engine instance
*
* @param Engine<FlightTemplate> $engine Vroom vroom!
*/
public static function setEngine(Engine $engine): void
{
self::$engine = $engine;
}
}

@ -1,10 +0,0 @@
<?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__)]);

@ -1,241 +0,0 @@
<?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;
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
require_once 'flight/Flight.php';
// require 'flight/autoload.php';
Flight::route('/', function () {
echo 'hello world!';
});
Flight::start();

@ -21,7 +21,6 @@
<exclude name="PSR12.Classes.AnonClassDeclaration.SpaceAfterKeyword"/>
</rule>
<file>index.php</file>
<file>flight</file>
<file>src</file>
<file>tests</file>
</ruleset>

@ -5,6 +5,5 @@ includes:
parameters:
level: 6
paths:
- flight
- index.php
- src
treatPhpDocTypesAsCertain: false

@ -1,13 +0,0 @@
hideManual: true
watch:
directories:
- tests
- flight
fileMask: '*.php'
notifications:
passingTests: false
failingTests: false
phpunit:
binaryPath: ./vendor/bin/phpunit
arguments: '--stop-on-failure'
timeout: 180

@ -0,0 +1,13 @@
hideManual: true
watch:
directories:
- tests
- src
fileMask: '*.php'
notifications:
passingTests: false
failingTests: false
phpunit:
binaryPath: ./vendor/bin/phpunit
arguments: '--stop-on-failure'
timeout: 180

@ -13,16 +13,16 @@
colors="true">
<coverage processUncoveredFiles="false">
<include>
<directory suffix=".php">flight/</directory>
<directory suffix=".php">src/</directory>
</include>
<exclude>
<file>flight/autoload.php</file>
<file>src/autoload.php</file>
</exclude>
</coverage>
<testsuites>
<testsuite name="default">
<directory>tests/</directory>
<exclude>tests/named-arguments/</exclude>
<exclude>tests/named_arguments/</exclude>
</testsuite>
</testsuites>
<logging />

@ -25,22 +25,23 @@ use Psr\Container\ContainerInterface;
* and generating an HTTP response.
*
* @license MIT, https://docs.flightphp.com/license
* @copyright Copyright (c) 2011-2025, Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com>
* @copyright Copyright (c) 2011-2026,
* Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com>, fadrian06 <https://github.com/fadrian06>
*
* @method void start()
* @method void stop()
* @method void stop(?int $code = null)
* @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true)
* @method EventDispatcher eventDispatcher()
* @method Route route(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '')
* @method void group(string $pattern, callable $callback, array $group_middlewares = [])
* @method Route post(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '')
* @method Route put(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '')
* @method Route patch(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '')
* @method Route delete(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '')
* @method void resource(string $pattern, string $controllerClass, array $methods = [])
* @method Route route(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @method void group(string $pattern, callable $callback, array<int, class-string|callable|array{0: class-string, 1: string}> $group_middlewares = [])
* @method Route post(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @method Route put(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @method Route patch(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @method Route delete(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @method void resource(string $pattern, class-string $controllerClass, array<string, string|array<int, string>> $methods = [])
* @method Router router()
* @method string getUrl(string $alias)
* @method void render(string $file, array $data = null, string $key = null)
* @method string getUrl(string $alias, array<string, mixed> $params)
* @method void render(string $file, ?array<string, mixed> $data, string $key = null)
* @method View view()
* @method void onEvent(string $event, callable $callback)
* @method void triggerEvent(string $event, ...$args)
@ -56,35 +57,10 @@ use Psr\Container\ContainerInterface;
* @method void etag(string $id, string $type = 'strong')
* @method void lastModified(int $time)
* @method void download(string $filePath)
*
* @phpstan-template EngineTemplate of object
* @phpstan-method void registerContainerHandler(ContainerInterface|callable(class-string<EngineTemplate> $id, array<int|string, mixed> $params): ?EngineTemplate $containerHandler)
* @phpstan-method Route route(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method void group(string $pattern, callable $callback, (class-string|callable|array{0: class-string, 1: string})[] $group_middlewares = [])
* @phpstan-method Route post(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method Route put(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method Route patch(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method Route delete(string $pattern, callable|string|array{0: class-string, 1: string} $callback, bool $pass_route = false, string $alias = '')
* @phpstan-method void resource(string $pattern, class-string $controllerClass, array<string, string|array<string>> $methods = [])
* @phpstan-method string getUrl(string $alias, array<string, mixed> $params = [])
* @phpstan-method void before(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* @phpstan-method void after(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* @phpstan-method void set(string|iterable<string, mixed> $key, ?mixed $value = null)
* @phpstan-method mixed get(?string $key)
* @phpstan-method void render(string $file, ?array<string, mixed> $data = null, ?string $key = null)
* @phpstan-method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @phpstan-method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @phpstan-method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
*
* Note: IDEs will use standard @method tags for autocompletion, while PHPStan will use @phpstan-* tags for advanced type checking.
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class Engine
{
/**
* @var array<string> List of methods that can be extended in the Engine class.
*/
/** @var array<int, string> List of methods that can be extended in the Engine class */
private const MAPPABLE_METHODS = [
'start',
'stop',
@ -112,53 +88,76 @@ class Engine
'triggerEvent'
];
/** @var array<string, mixed> Stored variables. */
protected array $vars = [];
/** @var array<string, mixed> */
private array $vars = [
'flight.base_url' => null,
'flight.case_sensitive' => false,
'flight.handle_errors' => true,
'flight.log_errors' => false,
'flight.views.path' => './views',
'flight.views.extension' => '.php',
'flight.content_length' => true,
];
/** Class loader. */
protected Loader $loader;
/** @var Dispatcher<EngineTemplate> Method and class dispatcher. */
protected Dispatcher $dispatcher;
/** Event dispatcher. */
protected EventDispatcher $eventDispatcher;
/** If the framework has been initialized or not. */
protected bool $initialized = false;
/** If the request has been handled or not. */
protected bool $requestHandled = false;
/** If the request has been handled or not */
private bool $requestHandled = false;
public function __construct()
{
$this->loader = new Loader();
$this->dispatcher = new Dispatcher();
$this->init();
// Register default components
$this->loader->register('eventDispatcher', EventDispatcher::class);
$this->loader->register('request', Request::class);
$this->loader->register('response', Response::class, [], function (Response $response): void {
$response->content_length = $this->get('flight.content_length');
});
$this->loader->register('router', Router::class, [], function (Router $router): void {
$router->caseSensitive = $this->vars['flight.case_sensitive'];
});
$this->loader->register('view', View::class, [], function (View $view): void {
$view->path = $this->vars['flight.views.path'];
$view->extension = $this->vars['flight.views.extension'];
});
foreach (self::MAPPABLE_METHODS as $name) {
$this->dispatcher->set($name, [$this, "_$name"]);
}
// Enable error handling
if ($this->get('flight.handle_errors')) {
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
}
}
/**
* Handles calls to class methods.
*
* @param string $name Method name
* @param array<int, mixed> $params Method parameters
*
* @throws Exception
* @return mixed Callback results
* @param array<int, mixed> $arguments Method parameters
* @throws Throwable
* @return mixed
*/
public function __call(string $name, array $params)
public function __call(string $name, array $arguments)
{
$callback = $this->dispatcher->get($name);
if (\is_callable($callback)) {
return $this->dispatcher->run($name, $params);
if (is_callable($callback)) {
return $this->dispatcher->run($name, $arguments);
}
if (!$this->loader->get($name)) {
throw new Exception("$name must be a mapped method.");
}
$shared = empty($params) || $params[0];
$shared = empty($arguments) || $arguments[0];
return $this->loader->load($name, $shared);
}
@ -167,79 +166,14 @@ class Engine
// Core Methods //
//////////////////
/** Initializes the framework. */
public function init(): void
{
$initialized = $this->initialized;
$self = $this;
if ($initialized) {
$this->vars = [];
$this->loader->reset();
$this->dispatcher->reset();
}
// Add this class to Dispatcher
$this->dispatcher->setEngine($this);
// Register default components
$this->map('eventDispatcher', function () {
return EventDispatcher::getInstance();
});
$this->loader->register('request', Request::class);
$this->loader->register('response', Response::class);
$this->loader->register('router', Router::class);
$this->loader->register('view', View::class, [], function (View $view) use ($self) {
$view->path = $self->get('flight.views.path');
$view->extension = $self->get('flight.views.extension');
});
foreach (self::MAPPABLE_METHODS as $name) {
$this->dispatcher->set($name, [$this, "_$name"]);
}
// Default configuration settings
$this->set('flight.base_url');
$this->set('flight.case_sensitive', false);
$this->set('flight.handle_errors', true);
$this->set('flight.log_errors', false);
$this->set('flight.views.path', './views');
$this->set('flight.views.extension', '.php');
$this->set('flight.content_length', true);
$this->set('flight.v2.output_buffering', false);
// Startup configuration
$this->before('start', function () use ($self) {
// Enable error handling
if ($self->get('flight.handle_errors')) {
set_error_handler([$self, 'handleError']);
set_exception_handler([$self, 'handleException']);
}
// Set case-sensitivity
$self->router()->caseSensitive = $self->get('flight.case_sensitive');
// Set Content-Length
$self->response()->content_length = $self->get('flight.content_length');
// This is to maintain legacy handling of output buffering
// which causes a lot of problems. This will be removed
// in v4
$self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering');
});
$this->initialized = true;
}
/**
* Custom error handler. Converts errors into exceptions.
*
* @param int $errno Error number
* @param string $errstr Error string
* @param string $errfile Error file name
* @param int $errline Error file line number
*
* @return false
* Converts errors into exceptions
* @param int $errno Level of the error raised.
* @param string $errstr Error message.
* @param string $errfile Filename that the error was raised in.
* @param int $errline Line number where the error was raised.
* @throws ErrorException
* @return false
*/
public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
{
@ -250,27 +184,21 @@ class Engine
return false;
}
/**
* Custom exception handler. Logs exceptions.
*
* @param Throwable $e Thrown exception
*/
public function handleException(Throwable $e): void
/** Logs exceptions */
public function handleException(Throwable $ex): void
{
if ($this->get('flight.log_errors')) {
error_log($e->getMessage()); // @codeCoverageIgnore
error_log($ex->getMessage());
}
$this->error($e);
$this->error($ex);
}
/**
* Registers the container handler
*
* @param ContainerInterface|callable(class-string<T> $id, array<int|string, mixed> $params): ?T $containerHandler
* Callback function or PSR-11 Container object that sets the container and how it will inject classes
*
* @template T of object
* @param ContainerInterface|callable(class-string<T>): T $containerHandler
* Callback function or PSR-11 Container object that sets the container and how it will inject classes
*/
public function registerContainerHandler($containerHandler): void
{
@ -278,25 +206,26 @@ class Engine
}
/**
* Maps a callback to a framework method.
*
* @param string $name Method name
* @param callable $callback Callback function
*
* Maps a callback to a framework method
* @throws Exception If trying to map over a framework method
*/
public function map(string $name, callable $callback): void
{
if (method_exists($this, $name)) {
$this->ensureMethodNotExists($name)->dispatcher->set($name, $callback);
}
/** @throws Exception */
private function ensureMethodNotExists(string $method): self
{
if (method_exists($this, $method)) {
throw new Exception('Cannot override an existing framework method.');
}
$this->dispatcher->set($name, $callback);
return $this;
}
/**
* Registers a class to a framework method.
*
* # Usage example:
* ```
* $app = new Engine;
@ -304,57 +233,47 @@ class Engine
*
* $app->user(); # <- Return a User instance
* ```
*
* @template T of object
* @param string $name Method name
* @param class-string<T> $class Class name
* @param array<int, mixed> $params Class initialization parameters
* @param ?Closure(T $instance): void $callback Function to call after object instantiation
*
* @template T of object
* @param ?callable(T): void $callback Function to call after object instantiation
* @throws Exception If trying to map over a framework method
*/
public function register(string $name, string $class, array $params = [], ?callable $callback = null): void
{
if (method_exists($this, $name)) {
throw new Exception('Cannot override an existing framework method.');
}
$this->loader->register($name, $class, $params, $callback);
$this->ensureMethodNotExists($name)->loader->register($name, $class, $params, $callback);
}
/** Unregisters a class to a framework method. */
/** Unregisters a class to a framework method */
public function unregister(string $methodName): void
{
$this->loader->unregister($methodName);
}
/**
* Adds a pre-filter to a method.
*
* Adds a pre-filter to a method
* @param string $name Method name
* @param Closure(array<int, mixed> &$params, string &$output): (void|false) $callback
* @param callable(array<int, mixed> &$params, string &$output): (void|false) $callback
*/
public function before(string $name, callable $callback): void
{
$this->dispatcher->hook($name, 'before', $callback);
$this->dispatcher->hook($name, Dispatcher::FILTER_BEFORE, $callback);
}
/**
* Adds a post-filter to a method.
*
* Adds a post-filter to a method
* @param string $name Method name
* @param Closure(array<int, mixed> &$params, string &$output): (void|false) $callback
* @param callable(array<int, mixed> &$params, string &$output): (void|false) $callback
*/
public function after(string $name, callable $callback): void
{
$this->dispatcher->hook($name, 'after', $callback);
$this->dispatcher->hook($name, Dispatcher::FILTER_AFTER, $callback);
}
/**
* Gets a variable.
*
* Gets a variable
* @param ?string $key Variable name
*
* @return mixed Variable value or `null` if `$key` doesn't exists.
*/
public function get(?string $key = null)
@ -367,15 +286,14 @@ class Engine
}
/**
* Sets a variable.
*
* Sets a variable
* @param string|iterable<string, mixed> $key
* Variable name as `string` or an iterable of `'varName' => $varValue`
* @param ?mixed $value Ignored if `$key` is an `iterable`
* @param mixed $value Ignored if `$key` is an `iterable`
*/
public function set($key, $value = null): void
{
if (\is_iterable($key)) {
if (is_iterable($key)) {
foreach ($key as $k => $v) {
$this->vars[$k] = $v;
}
@ -387,10 +305,8 @@ class Engine
}
/**
* Checks if a variable has been set.
*
* Checks if a variable has been set
* @param string $key Variable name
*
* @return bool Variable status
*/
public function has(string $key): bool
@ -399,8 +315,7 @@ class Engine
}
/**
* Unsets a variable. If no key is passed in, clear all variables.
*
* Unsets a variable. If no key is passed in, clear all variables
* @param ?string $key Variable name, if `$key` isn't provided, it clear all variables.
*/
public function clear(?string $key = null): void
@ -414,22 +329,11 @@ class Engine
}
/**
* Adds a path for class autoloading.
*
* @param string $dir Directory path
*/
public function path(string $dir): void
{
$this->loader->addDirectory($dir);
}
/**
* Processes each routes middleware.
*
* Processes each routes middleware
* @param Route $route The route to process the middleware for.
* @param string $eventName If this is the before or after method.
*/
protected function processMiddleware(Route $route, string $eventName): bool
private function processMiddleware(Route $route, string $eventName): bool
{
$atLeastOneMiddlewareFailed = false;
@ -437,6 +341,7 @@ class Engine
$middlewares = $eventName === Dispatcher::FILTER_BEFORE
? $route->middleware
: array_reverse($route->middleware);
$params = $route->params;
foreach ($middlewares as $middleware) {
@ -444,23 +349,23 @@ class Engine
$middlewareObject = false;
// Closure functions can only run on the before event
if ($eventName === Dispatcher::FILTER_BEFORE && is_object($middleware) === true && ($middleware instanceof Closure)) {
if ($eventName === Dispatcher::FILTER_BEFORE && is_object($middleware) && $middleware instanceof Closure) {
$middlewareObject = $middleware;
// If the object has already been created, we can just use it if the event name exists.
} elseif (is_object($middleware) === true) {
$middlewareObject = method_exists($middleware, $eventName) === true ? [$middleware, $eventName] : false;
} elseif (is_object($middleware)) {
$middlewareObject = method_exists($middleware, $eventName) ? [$middleware, $eventName] : false;
// If the middleware is a string, we need to create the object and then call the event.
} elseif (is_string($middleware) === true && method_exists($middleware, $eventName) === true) {
} elseif (is_string($middleware) && method_exists($middleware, $eventName)) {
$resolvedClass = null;
// if there's a container assigned, we should use it to create the object
if ($this->dispatcher->mustUseContainer($middleware) === true) {
if ($this->dispatcher->mustUseContainer($middleware)) {
$resolvedClass = $this->dispatcher->resolveContainerClass($middleware, $params);
// otherwise just assume it's a plain jane class, so inject the engine
// just like in Dispatcher::invokeCallable()
} elseif (class_exists($middleware) === true) {
} elseif (class_exists($middleware)) {
$resolvedClass = new $middleware($this);
}
@ -471,16 +376,14 @@ class Engine
}
// If nothing was resolved, go to the next thing
if ($middlewareObject === false) {
if (!$middlewareObject) {
continue;
}
// This is the way that v3 handles output buffering (which captures output correctly)
$useV3OutputBuffering =
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
$useV3OutputBuffering = !$route->is_streamed;
if ($useV3OutputBuffering === true) {
if ($useV3OutputBuffering) {
ob_start();
}
@ -498,7 +401,7 @@ class Engine
microtime(true) - $start
);
if ($useV3OutputBuffering === true) {
if ($useV3OutputBuffering) {
$this->response()->write(ob_get_clean());
}
@ -550,17 +453,6 @@ class Engine
$response = $this->response();
$router = $this->router();
if ($response->v2_output_buffering === true) {
// Flush any existing output
if (ob_get_length() > 0) {
$response->write(ob_get_clean()); // @codeCoverageIgnore
}
// Enable output buffering
// This is closed in the Engine->_stop() method
ob_start();
}
// Route the request
$failedMiddlewareCheck = false;
while ($route = $router->route($request)) {
@ -610,22 +502,18 @@ class Engine
$this->triggerEvent('flight.middleware.before', $route);
}
$useV3OutputBuffering =
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
$useV3OutputBuffering = !$route->is_streamed;
if ($useV3OutputBuffering === true) {
if ($useV3OutputBuffering) {
ob_start();
}
// Call route handler
$routeStart = microtime(true);
$continue = $this->dispatcher->execute(
$route->callback,
$params
);
$continue = $this->dispatcher->execute($route->callback, $params);
$this->triggerEvent('flight.route.executed', $route, microtime(true) - $routeStart);
if ($useV3OutputBuffering === true) {
if ($useV3OutputBuffering) {
$response->write(ob_get_clean());
}
@ -638,6 +526,7 @@ class Engine
$failedMiddlewareCheck = true;
break;
}
$this->triggerEvent('flight.middleware.after', $route);
}
@ -648,7 +537,6 @@ class Engine
}
$router->next();
$dispatched = false;
}
@ -662,7 +550,7 @@ class Engine
} elseif ($dispatched === false) {
// Get the previous route and check if the method failed, but the URL was good.
$lastRouteExecuted = $router->executedRoute;
if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) === true && $lastRouteExecuted->matchMethod($request->method) === false) {
if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) && !$lastRouteExecuted->matchMethod($request->method)) {
$this->methodNotFound($lastRouteExecuted);
} else {
$this->notFound();
@ -720,10 +608,6 @@ class Engine
$response->status($code);
}
if ($response->v2_output_buffering === true && ob_get_length() > 0) {
$response->write(ob_get_clean());
}
$response->send();
}
}
@ -948,9 +832,6 @@ class Engine
->status($code)
->header('Content-Type', 'application/json')
->write($json);
if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
}
/**
@ -973,10 +854,8 @@ class Engine
): void {
$this->json($data, $code, $encode, $charset, $option);
$jsonBody = $this->response()->getBody();
if ($this->response()->v2_output_buffering === false) {
$this->response()->clearBody();
$this->response()->send();
}
$this->response()->clearBody();
$this->response()->send();
$this->halt($code, $jsonBody, empty(getenv('PHPUNIT_TEST')));
}
@ -1007,9 +886,6 @@ class Engine
->status($code)
->header('Content-Type', 'application/javascript; charset=' . $charset)
->write($callback . '(' . $json . ');');
if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
}
/**

@ -0,0 +1,309 @@
<?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;
use flight\core\EventDispatcher;
use Psr\Container\ContainerInterface;
/**
* The Flight class is a static representation of the framework.
*
* @license MIT, https://docs.flightphp.com/license
* @copyright Copyright (c) 2011-2026,
* Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com>, fadrian06 <https://github.com/fadrian06>
*/
// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace
class Flight
{
private static Engine $engine;
/**
* @param array<int, mixed> $arguments
* @return mixed
* @throws Throwable
*/
public static function __callStatic(string $name, array $arguments)
{
return self::app()->{$name}(...$arguments);
}
public static function app(): Engine
{
return self::$engine ?? self::$engine = new Engine();
}
public static function setEngine(Engine $engine): void
{
self::$engine = $engine;
}
public static function start(): void
{
self::app()->start();
}
public static function stop(?int $code = null): void
{
self::app()->stop($code);
}
public static function halt(int $code = 200, string $message = '', bool $actuallyExit = true): void
{
self::app()->halt($code, $message, $actuallyExit);
}
/**
* @template T of object
* @param class-string<T> $class
* @param mixed[] $params
* @param ?callable(T): void $callback
*/
public static function register(string $name, string $class, array $params = [], ?callable $callback = null): void
{
self::app()->register($name, $class, $params, $callback);
}
public static function unregister(string $methodName): void
{
self::app()->unregister($methodName);
}
/**
* @template T of object
* @param ContainerInterface|callable(class-string<T>, mixed[]): ?T $containerHandler
*/
public static function registerContainerHandler($containerHandler): void
{
self::app()->registerContainerHandler($containerHandler);
}
public static function eventDispatcher(): EventDispatcher
{
return self::app()->eventDispatcher();
}
/** @param callable|string|array{0: class-string, 1: string} $callback */
public static function route(
string $pattern,
$callback,
bool $pass_route = false,
string $alias = ''
): Route {
return self::app()->route($pattern, $callback, $pass_route, $alias);
}
/** @param array<int, class-string|callable|array{0: class-string, 1: string}> $group_middlewares */
public static function group(string $pattern, callable $callback, array $group_middlewares = []): void
{
self::app()->group($pattern, $callback, $group_middlewares);
}
/** @param callable|string|array{0: class-string, 1: string} $callback */
public static function post(
string $pattern,
$callback,
bool $pass_route = false,
string $alias = ''
): Route {
return self::app()->post($pattern, $callback, $pass_route, $alias);
}
/** @param callable|string|array{0: class-string, 1: string} $callback */
public static function put(
string $pattern,
$callback,
bool $pass_route = false,
string $alias = ''
): Route {
return self::app()->put($pattern, $callback, $pass_route, $alias);
}
/** @param callable|string|array{0: class-string, 1: string} $callback */
public static function patch(
string $pattern,
$callback,
bool $pass_route = false,
string $alias = ''
): Route {
return self::app()->patch($pattern, $callback, $pass_route, $alias);
}
/** @param callable|string|array{0: class-string, 1: string} $callback */
public static function delete(
string $pattern,
$callback,
bool $pass_route = false,
string $alias = ''
): Route {
return self::app()->delete($pattern, $callback, $pass_route, $alias);
}
/**
* @param class-string $controllerClass
* @param array<string, string|array<int, string>> $methods
*/
public static function resource(string $pattern, string $controllerClass, array $methods = []): void
{
self::app()->resource($pattern, $controllerClass, $methods);
}
public static function router(): Router
{
return self::app()->router();
}
/** @param array<string, mixed> $params */
public static function getUrl(string $alias, array $params = []): string
{
return self::app()->getUrl($alias, $params);
}
public static function map(string $name, callable $callback): void
{
self::app()->map($name, $callback);
}
/** @param callable(array<int, mixed> &$params, string &$output): (void|false) $callback */
public static function before(string $name, callable $callback): void
{
self::app()->before($name, $callback);
}
/** @param callable(array<int, mixed> &$params, string &$output): (void|false) $callback */
public static function after(string $name, callable $callback): void
{
self::app()->after($name, $callback);
}
/**
* @param string|iterable<string, mixed> $key
* @param mixed $value
*/
public static function set($key, $value): void
{
self::app()->set($key, $value);
}
/** @return mixed */
public static function get(?string $key = null)
{
return self::app()->get($key);
}
public static function has(string $key): bool
{
return self::app()->has($key);
}
public static function clear(?string $key = null): void
{
self::app()->clear($key);
}
/** @param ?array<string, mixed> $data */
public static function render(string $file, ?array $data = null, ?string $key = null): void
{
self::app()->render($file, $data, $key);
}
public static function view(): View
{
return self::app()->view();
}
public static function onEvent(string $event, callable $callback): void
{
self::app()->onEvent($event, $callback);
}
/** @param mixed ...$args */
public static function triggerEvent(string $event, ...$args): void
{
self::app()->triggerEvent($event, ...$args);
}
public static function request(): Request
{
return self::app()->request();
}
public static function response(): Response
{
return self::app()->response();
}
public static function redirect(string $url, int $code = 303): void
{
self::app()->redirect($url, $code);
}
/** @param mixed $data */
public static function json(
$data,
int $code = 200,
bool $encode = true,
string $charset = 'utf8',
int $encodeOption = 0
): void {
self::app()->json($data, $code, $encode, $charset, $encodeOption);
}
/** @param mixed $data */
public static function jsonHalt(
$data,
int $code = 200,
bool $encode = true,
string $charset = 'utf8',
int $encodeOption = 0
): void {
self::app()->jsonHalt($data, $code, $encode, $charset, $encodeOption);
}
/** @param mixed $data */
public static function jsonp(
$data,
string $param = 'jsonp',
int $code = 200,
bool $encode = true,
string $charset = 'utf8',
int $encodeOption = 0
): void {
self::app()->jsonp($data, $param, $code, $encode, $charset, $encodeOption);
}
public static function error(Throwable $exception): void
{
self::app()->error($exception);
}
public static function notFound(): void
{
self::app()->notFound();
}
public function methodNotFound(Route $route): void
{
self::app()->methodNotFound($route);
}
public static function etag(string $id, string $type = 'strong'): void
{
self::app()->etag($id, $type);
}
public static function lastModified(int $time): void
{
self::app()->lastModified($time);
}
public static function download(string $filePath): void
{
self::app()->download($filePath);
}
}

@ -18,9 +18,8 @@ use TypeError;
* allows you to hook other functions to an event that can modify the
* input parameters and/or the output.
*
* @license MIT, http://flightphp.com/license
* @license MIT, https://docs.flightphp.com/license/
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @phpstan-template EngineTemplate of object
*/
class Dispatcher
{
@ -30,7 +29,6 @@ class Dispatcher
/** Exception message if thrown by setting the container as a callable method. */
protected ?Throwable $containerException = null;
/** @var ?Engine<EngineTemplate> $engine Engine instance. */
protected ?Engine $engine = null;
/** @var array<string, callable(): (void|mixed)> Mapped events. */
@ -76,13 +74,6 @@ class Dispatcher
);
}
/**
* Sets the engine instance
*
* @param Engine<EngineTemplate> $engine Flight instance
*
* @return void
*/
public function setEngine(Engine $engine): void
{
$this->engine = $engine;
@ -160,14 +151,7 @@ class Dispatcher
return $output;
}
/**
* Assigns a callback to an event.
*
* @param string $name Event name.
* @param callable(): (void|mixed) $callback Callback function.
*
* @return $this
*/
/** Assigns a callback to an event */
public function set(string $name, callable $callback): self
{
$this->events[$name] = $callback;
@ -175,13 +159,7 @@ class Dispatcher
return $this;
}
/**
* Gets an assigned callback.
*
* @param string $name Event name.
*
* @return null|(callable(): (void|mixed)) $callback Callback function.
*/
/** Gets an assigned callback */
public function get(string $name): ?callable
{
return $this->events[$name] ?? null;
@ -500,11 +478,7 @@ class Dispatcher
}
}
/**
* Resets the object to the initial state.
*
* @return $this
*/
/** Resets the object to the initial state */
public function reset(): self
{
$this->events = [];

@ -6,51 +6,44 @@ namespace flight\core;
class EventDispatcher
{
/** @var self|null Singleton instance of the EventDispatcher */
/** Singleton instance of the EventDispatcher */
private static ?self $instance = null;
/** @var array<string, array<int, callable>> */
/** @var array<string, array<int, callable(): mixed>> */
protected array $listeners = [];
/**
* Singleton instance of the EventDispatcher.
*
* @return self
*/
/** Singleton instance of the EventDispatcher */
public static function getInstance(): self
{
if (self::$instance === null) {
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Register a callback for an event.
*
* @param string $event Event name
* @param callable $callback Callback function
* @param callable(): mixed $callback Callback function
*/
public function on(string $event, callable $callback): void
{
if (isset($this->listeners[$event]) === false) {
$this->listeners[$event] = [];
}
$this->listeners[$event] ??= [];
$this->listeners[$event][] = $callback;
}
/**
* Trigger an event with optional arguments.
*
* @param string $event Event name
* @param mixed ...$args Arguments to pass to the callbacks
*
* @return mixed
*/
public function trigger(string $event, ...$args)
{
$result = null;
if (isset($this->listeners[$event]) === true) {
if (isset($this->listeners[$event])) {
foreach ($this->listeners[$event] as $callback) {
$result = call_user_func_array($callback, $args);
@ -60,27 +53,24 @@ class EventDispatcher
}
}
}
return $result;
}
/**
* Check if an event has any registered listeners.
*
* @param string $event Event name
*
* @return bool True if the event has listeners, false otherwise
*/
public function hasListeners(string $event): bool
{
return isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0;
return isset($this->listeners[$event]) && count($this->listeners[$event]) > 0;
}
/**
* Get all listeners registered for a specific event.
*
* @param string $event Event name
*
* @return array<int, callable> Array of callbacks registered for the event
* @return array<int, callable(): mixed> Array of callbacks registered for the event
*/
public function getListeners(string $event): array
{
@ -89,7 +79,6 @@ class EventDispatcher
/**
* Get a list of all events that have registered listeners.
*
* @return array<int, string> Array of event names
*/
public function getAllRegisteredEvents(): array
@ -99,41 +88,31 @@ class EventDispatcher
/**
* Remove a specific listener for an event.
*
* @param string $event the event name
* @param callable $callback the exact callback to remove
*
* @return void
* @param callable(): mixed $callback the exact callback to remove
*/
public function removeListener(string $event, callable $callback): void
{
if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) {
$this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) {
return $listener !== $callback;
});
if ($this->hasListeners($event)) {
$this->listeners[$event] = array_filter(
$this->listeners[$event],
static fn(callable $listener): bool => $listener !== $callback,
);
$this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array
}
}
/**
* Remove all listeners for a specific event.
*
* @param string $event the event name
*
* @return void
*/
public function removeAllListeners(string $event): void
{
if (isset($this->listeners[$event]) === true) {
unset($this->listeners[$event]);
}
unset($this->listeners[$event]);
}
/**
* Remove the current singleton instance of the EventDispatcher.
*
* @return void
*/
/** Remove the current singleton instance of the EventDispatcher. */
public static function resetInstance(): void
{
self::$instance = null;

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace flight\core;
use Throwable;
/**
* 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.
* @license MIT, https://docs.flightphp.com/license/
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Loader
{
/** @var array<string, array{class-string|callable(): object, array<int, mixed>, ?callable(object): void}> */
protected array $classes = [];
/** @var array<string, object> */
protected array $instances = [];
/**
* @template T of object
* @param class-string<T>|callable(): T $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param ?callable(T): void $callback $callback Function to call after object instantiation
*/
public function register(
string $alias,
$class,
array $params = [],
?callable $callback = null
): void {
$this->classes[$alias] = [$class, $params, $callback];
unset($this->instances[$alias]);
}
public function unregister(string $alias): void
{
unset($this->classes[$alias]);
}
/**
* @throws Throwable
* @return ?object Class instance
*/
public function load(string $alias, bool $shared = true): ?object
{
if (!key_exists($alias, $this->classes)) {
return null;
}
[$class, $params, $callback] = $this->classes[$alias];
$instanceExists = key_exists($alias, $this->instances);
$obj = $shared && $instanceExists
? $this->instances[$alias] ?? null
: $this->instances[$alias] = $this->newInstance($class, $params);
if ($callback && (!$shared || !$instanceExists)) {
$callback($obj);
}
return $obj;
}
/**
* Gets a new instance of a class
* @template T of object
* @param class-string<T>|callable(): T $class Class name or callback function to instantiate class
* @param array<int, string> $params Class initialization parameters
* @throws Throwable
* @return T Class instance
*/
public function newInstance($class, array $params = []): object
{
return is_callable($class) ? $class(...$params) : new $class(...$params);
}
/**
* Gets a registered callable
* @return ?array{class-string|callable(): object, array<int, mixed>, ?callable(object): void}
* Class information or null if not registered
*/
public function get(string $alias): ?array
{
return $this->classes[$alias] ?? null;
}
/** Resets the object to the initial state */
public function reset(): self
{
$this->classes = [];
$this->instances = [];
return $this;
}
}

@ -22,15 +22,6 @@ class Response
*/
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
*
@ -271,7 +262,7 @@ class Response
$this->clearBody();
// This needs to clear the output buffer if it's on
if ($this->v2_output_buffering === false && ob_get_length() > 0) {
if (ob_get_length() > 0) {
ob_clean();
}
@ -421,18 +412,8 @@ class 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
}
}
$start = microtime(true);
// Only for the v3 output buffering.
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
}
$this->processResponseCallbacks();
if ($this->headersSent() === false) {
$this->sendHeaders();

@ -15,7 +15,6 @@ class AutoloadTest extends TestCase
protected function setUp(): void
{
$this->app = new Engine();
$this->app->path(__DIR__ . '/classes');
}
// Autoload a class

@ -16,7 +16,7 @@ class DocExamplesTest extends TestCase
{
$_SERVER = [];
$_REQUEST = [];
Flight::init();
Flight::app();
Flight::setEngine(new Engine());
}
@ -45,25 +45,6 @@ class DocExamplesTest extends TestCase
$this->assertEquals('[]', Flight::response()->getBody());
}
public function testMapNotFoundMethodV2OutputBuffering(): void
{
Flight::map('notFound', function () {
Flight::json([], 404);
});
Flight::request()->url = '/not-found';
Flight::route('/', function () {
echo 'hello world!';
});
Flight::set('flight.v2.output_buffering', true);
Flight::start();
ob_get_clean();
$this->assertEquals(404, Flight::response()->status());
$this->assertEquals('[]', Flight::response()->getBody());
}
public function testMapErrorMethod(): void
{
Flight::map('error', function (Throwable $error) {
@ -81,12 +62,9 @@ class DocExamplesTest extends TestCase
Flight::request()->method = 'GET';
Flight::request()->url = '/';
$router->get(
'/',
function () {
Flight::response()->write('from resp ');
}
);
$router->get('/', static function () {
Flight::response()->write('from resp ');
});
Flight::start();

@ -32,13 +32,7 @@ class EngineTest extends TestCase
public function testInitBeforeStart(): void
{
$engine = new class extends Engine {
public function getInitializedVar()
{
return $this->initialized;
}
};
$this->assertTrue($engine->getInitializedVar());
$engine = new Engine;
// we need to setup a dummy route
$engine->route('/someRoute', function () {});
@ -49,25 +43,6 @@ class EngineTest extends TestCase
$this->assertTrue($engine->response()->content_length);
}
public function testInitBeforeStartV2OutputBuffering(): void
{
$engine = new class extends Engine {
public function getInitializedVar(): bool
{
return $this->initialized;
}
};
$engine->set('flight.v2.output_buffering', true);
$this->assertTrue($engine->getInitializedVar());
$engine->start();
// This is a necessary evil because of how the v2 output buffer works.
ob_end_clean();
$this->assertFalse($engine->router()->caseSensitive);
$this->assertTrue($engine->response()->content_length);
}
public function testHandleErrorNoErrorNumber(): void
{
$engine = new Engine();
@ -153,26 +128,6 @@ class EngineTest extends TestCase
$engine->start();
}
public function testStartWithRouteButReturnedValueThrows404V2OutputBuffering(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/someRoute';
$engine = new class extends Engine {
public function getInitializedVar(): bool
{
return $this->initialized;
}
};
$engine->set('flight.v2.output_buffering', true);
$engine->route('/someRoute', function () {
echo 'i ran';
return true;
}, true);
$this->expectOutputString('<h1>404 Not Found</h1><h3>The page you have requested could not be found.</h3>');
$engine->start();
}
public function testDoubleReturnTrueRoutesContinueIteration(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
@ -322,34 +277,6 @@ class EngineTest extends TestCase
$this->assertEquals(500, $engine->response()->status());
}
public function testStopWithCodeV2OutputBuffering(): void
{
$engine = new class extends Engine {
public function getLoader()
{
return $this->loader;
}
};
// doing this so we can overwrite some parts of the response
$engine->getLoader()->register('response', function () {
return new class extends Response {
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self
{
return $this;
}
};
});
$engine->set('flight.v2.output_buffering', true);
$engine->route('/testRoute', function () use ($engine) {
echo 'I am a teapot';
$engine->stop(500);
});
$engine->request()->url = '/testRoute';
$engine->start();
$this->expectOutputString('I am a teapot');
$this->assertEquals(500, $engine->response()->status());
}
public function testPostRoute(): void
{
$engine = new Engine();
@ -519,16 +446,6 @@ class EngineTest extends TestCase
$engine->json(['key1' => 'value1', 'key2' => 'value2', 'utf8_emoji' => "\xB1\x31"]);
}
public function testJsonV2OutputBuffering(): void
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->json(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('{"key1":"value1","key2":"value2"}');
$this->assertEquals('application/json', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
}
public function testJsonHalt(): void
{
$engine = new Engine();
@ -549,17 +466,6 @@ class EngineTest extends TestCase
$this->assertEquals('whatever({"key1":"value1","key2":"value2"});', $engine->response()->getBody());
}
public function testJsonPV2OutputBuffering(): void
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->request()->query['jsonp'] = 'whatever';
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('whatever({"key1":"value1","key2":"value2"});');
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
}
public function testJsonpBadParam(): void
{
$engine = new Engine();
@ -569,16 +475,6 @@ class EngineTest extends TestCase
$this->assertEquals(200, $engine->response()->status());
}
public function testJsonpBadParamV2OutputBuffering(): void
{
$engine = new Engine();
$engine->response()->v2_output_buffering = true;
$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('({"key1":"value1","key2":"value2"});');
$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
}
public function testEtagSimple(): void
{
$engine = new Engine();

@ -15,7 +15,7 @@ class EventSystemTest extends TestCase
{
// Reset the Flight engine before each test to ensure a clean state
Flight::setEngine(new Engine());
Flight::app()->init();
Flight::app();
Flight::eventDispatcher()->resetInstance(); // Clear any existing listeners
}

@ -20,7 +20,7 @@ class FlightTest extends TestCase
{
$_SERVER = [];
$_REQUEST = [];
Flight::init();
Flight::app();
Flight::setEngine(new Engine());
Flight::set('flight.views.path', __DIR__ . '/views');
}
@ -71,8 +71,6 @@ class FlightTest extends TestCase
// Register a class
public function testRegister(): void
{
Flight::path(__DIR__ . '/classes');
Flight::register('user', User::class);
$user = Flight::user();
@ -277,25 +275,6 @@ class FlightTest extends TestCase
$this->assertEquals('test', Flight::response()->getBody());
}
public function testHookOutputBufferingV2OutputBuffering(): void
{
Flight::route('/test', function () {
echo 'test';
});
Flight::before('start', function ($output) {
echo 'hooked before start';
});
Flight::set('flight.v2.output_buffering', true);
Flight::request()->url = '/test';
$this->expectOutputString('hooked before starttest');
ob_start();
Flight::start();
$this->assertEquals('hooked before starttest', Flight::response()->getBody());
}
public function testStreamRoute(): void
{
$response_mock = new class extends Response {

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace tests;
use flight\core\Loader;
use tests\classes\Factory;
use tests\classes\User;
use PHPUnit\Framework\TestCase;
use tests\classes\User;
use tests\classes\Factory;
use tests\classes\TesterClass;
class LoaderTest extends TestCase
@ -17,63 +17,41 @@ class LoaderTest extends TestCase
protected function setUp(): void
{
$this->loader = new Loader();
$this->loader->autoload(true, __DIR__ . '/classes');
}
// Autoload a class
public function testAutoload(): void
{
$this->loader->register('tests', User::class);
$test = $this->loader->load('tests');
self::assertIsObject($test);
self::assertInstanceOf(User::class, $test);
}
// Register a class
public function testRegister(): void
{
$this->loader->register('a', User::class);
$user = $this->loader->load('a');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $user);
self::assertEquals('', $user->name);
self::assertSame('', $user->name);
}
// Register a class with constructor parameters
public function testRegisterWithConstructor(): void
{
$this->loader->register('b', User::class, ['Bob']);
$user = $this->loader->load('b');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $user);
self::assertEquals('Bob', $user->name);
self::assertSame('Bob', $user->name);
}
// Register a class with initialization
public function testRegisterWithInitialization(): void
{
$this->loader->register('c', User::class, ['Bob'], function ($user) {
$this->loader->register('c', User::class, ['Bob'], static function (User $user): void {
$user->name = 'Fred';
});
$user = $this->loader->load('c');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $user);
self::assertEquals('Fred', $user->name);
}
// Get a non-shared instance of a class
public function testSharedInstance(): void
{
$this->loader->register('d', User::class);
$user1 = $this->loader->load('d');
$user2 = $this->loader->load('d');
$user3 = $this->loader->load('d', false);
@ -82,38 +60,24 @@ class LoaderTest extends TestCase
self::assertNotSame($user1, $user3);
}
// Gets an object from a factory method
public function testRegisterUsingCallable(): void
{
$this->loader->register('e', ['\tests\classes\Factory', 'create']);
$this->loader->register('e', [Factory::class, 'create']);
$obj = $this->loader->load('e');
self::assertIsObject($obj);
self::assertInstanceOf(Factory::class, $obj);
$obj2 = $this->loader->load('e');
$obj3 = $this->loader->load('e', false);
self::assertIsObject($obj2);
self::assertInstanceOf(Factory::class, $obj2);
self::assertInstanceOf(Factory::class, $obj);
self::assertSame($obj, $obj2);
$obj3 = $this->loader->load('e', false);
self::assertIsObject($obj3);
self::assertInstanceOf(Factory::class, $obj3);
self::assertNotSame($obj, $obj3);
}
// Gets an object from a callback function
public function testRegisterUsingCallback(): void
{
$this->loader->register('f', function () {
return Factory::create();
});
$this->loader->register('f', static fn(): Factory => Factory::create());
$obj = $this->loader->load('f');
self::assertIsObject($obj);
self::assertInstanceOf(Factory::class, $obj);
}
@ -121,15 +85,22 @@ class LoaderTest extends TestCase
{
$this->loader->register('g', User::class);
$current_class = $this->loader->get('g');
$this->assertEquals([User::class, [], null], $current_class);
$this->assertSame([User::class, [], null], $current_class);
$this->loader->unregister('g');
$unregistered_class_result = $this->loader->get('g');
$this->assertNull($unregistered_class_result);
}
public function testNewInstance6Params(): void
{
$TesterClass = $this->loader->newInstance(TesterClass::class, ['Bob', 'Fred', 'Joe', 'Jane', 'Sally', 'Suzie']);
$TesterClass = $this->loader->newInstance(
TesterClass::class,
['Bob', 'Fred', 'Joe', 'Jane', 'Sally', 'Suzie']
);
$this->assertEquals('Bob', $TesterClass->param1);
$this->assertEquals('Fred', $TesterClass->param2);
$this->assertEquals('Joe', $TesterClass->param3);
@ -137,32 +108,4 @@ class LoaderTest extends TestCase
$this->assertEquals('Sally', $TesterClass->param5);
$this->assertEquals('Suzie', $TesterClass->param6);
}
public function testAddDirectoryAsArray(): void
{
$loader = new class extends Loader {
public function getDirectories()
{
return self::$dirs;
}
};
$loader->addDirectory([__DIR__ . '/classes']);
self::assertEquals([
dirname(__DIR__),
__DIR__ . '/classes'
], $loader->getDirectories());
}
public function testV2ClassLoading(): void
{
$loader = new class extends Loader {
public static function getV2ClassLoading()
{
return self::$v2ClassLoading;
}
};
$this->assertTrue($loader::getV2ClassLoading());
$loader::setV2ClassLoading(false);
$this->assertFalse($loader::getV2ClassLoading());
}
}

@ -21,7 +21,6 @@ class RenderTest extends TestCase
public function testRenderView(): void
{
$this->app->render('hello', ['name' => 'Bob']);
$this->expectOutputString('Hello, Bob!');
}

@ -4,17 +4,23 @@ declare(strict_types=1);
namespace tests\classes;
class TesterClass
final class TesterClass
{
public $param1;
public $param2;
public $param3;
public $param4;
public $param5;
public $param6;
public ?string $param1;
public ?string $param2;
public ?string $param3;
public ?string $param4;
public ?string $param5;
public ?string $param6;
public function __construct($param1, $param2, $param3, $param4, $param5, $param6)
{
public function __construct(
?string $param1,
?string $param2,
?string $param3,
?string $param4,
?string $param5,
?string $param6
) {
$this->param1 = $param1;
$this->param2 = $param2;
$this->param3 = $param3;

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace tests\classes;
class User
final class User
{
public string $name;

@ -26,7 +26,7 @@ class RouteCommandTest extends TestCase
$_SERVER = [];
$_REQUEST = [];
Flight::init();
Flight::app();
Flight::setEngine(new Engine());
}

@ -2,13 +2,10 @@
declare(strict_types=1);
namespace tests\groupcompactsyntax;
namespace tests\group_compact_syntax;
use Flight;
use PHPUnit\Framework\TestCase;
use tests\groupcompactsyntax\PostsController;
use tests\groupcompactsyntax\TodosController;
use tests\groupcompactsyntax\UsersController;
final class FlightRouteCompactSyntaxTest extends TestCase
{

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace tests\groupcompactsyntax;
namespace tests\group_compact_syntax;
final class PostsController
{

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace tests\groupcompactsyntax;
namespace tests\group_compact_syntax;
final class TodosController
{

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace tests\groupcompactsyntax;
namespace tests\group_compact_syntax;
final class UsersController
{

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace tests\named_arguments;
// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace
class ExampleClass
{

@ -2,10 +2,9 @@
declare(strict_types=1);
namespace Tests\PHP8;
namespace tests\named_arguments;
use DateTimeImmutable;
use ExampleClass;
use Flight;
use flight\Container;
use flight\Engine;

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Server;
namespace tests\server;
class AuthCheck
{

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Server;
namespace tests\server;
use Flight;

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Server;
namespace tests\server;
use Flight;

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Server;
namespace tests\server;
class Pascal_Snake_Case // phpcs:ignore
{

@ -7,10 +7,10 @@ use flight\core\Loader;
use flight\database\PdoWrapper;
use tests\classes\Container;
use tests\classes\ContainerDefault;
use Tests\Server\AuthCheck;
use Tests\Server\LayoutMiddleware;
use Tests\Server\OverwriteBodyMiddleware;
use Tests\Server\Pascal_Snake_Case;
use tests\server\AuthCheck;
use tests\server\LayoutMiddleware;
use tests\server\OverwriteBodyMiddleware;
use tests\server\Pascal_Snake_Case;
/*
* This is the test file where we can open up a quick test server and make

@ -9,7 +9,7 @@ declare(strict_types=1);
* @author Kristaps Muižnieks https://github.com/krmu
*/
namespace Tests\ServerV2 {
namespace tests\server_v2 {
class AuthCheck
{
public function before(): void
@ -23,7 +23,7 @@ namespace Tests\ServerV2 {
namespace {
use Tests\ServerV2\AuthCheck;
use tests\server_v2\AuthCheck;
require_once __DIR__ . '/../phpunit_autoload.php';
Loading…
Cancel
Save