pull/689/merge
fadrian06 12 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 indent_size = 4
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md] [*.md]
indent_size = 2 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 tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2` - Run test server: `composer test-server` or `composer test-server-v2`
- Lint code: `composer lint` (uses phpstan/phpstan, level 6) - Lint code: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1) - Format code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Check code style: `composer phpcs`
- Test coverage: `composer test-coverage` - Test coverage: `composer test-coverage`
## Coding Standards ## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS) - Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`) - Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance - PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

4
.gitattributes vendored

@ -5,9 +5,9 @@
/.gitattributes export-ignore /.gitattributes export-ignore
/.gitignore export-ignore /.gitignore export-ignore
/CONTRIBUTING.md export-ignore /CONTRIBUTING.md export-ignore
/index.php export-ignore
/phpcs.xml.dist export-ignore /phpcs.xml.dist export-ignore
/phpstan-baseline.neon export-ignore /phpstan-baseline.neon export-ignore
/phpstan.dist.neon export-ignore /phpstan.dist.neon export-ignore
/phpunit-watcher.yml export-ignore /phpunit-watcher.yml.dist export-ignore
/phpunit.xml.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 tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2` - Run test server: `composer test-server` or `composer test-server-v2`
- Lint code: `composer lint` (uses phpstan/phpstan, level 6) - Lint code: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1) - Format code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Check code style: `composer phpcs`
- Test coverage: `composer test-coverage` - Test coverage: `composer test-coverage`
## Coding Standards ## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS) - Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`) - Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance - PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) - 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* *.sublime*
clover.xml .phpunit.result.cache
/coverage/
/vendor/
composer.lock
phpcs.xml phpcs.xml
phpstan.neon phpstan.neon
phpunit-watcher.yml
phpunit.xml 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: * **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. * 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. * **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", "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", "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", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Mike Cao", "name": "Mike Cao",
"email": "mike@mikecao.com", "email": "mike@mikecao.com",
"homepage": "http://www.mikecao.com/", "homepage": "https://mikecao.com/",
"role": "Original Developer" "role": "Original Developer"
}, },
{ {
"name": "Franyer Sánchez", "name": "Franyer Sánchez",
"email": "franyeradriansanchez@gmail.com", "email": "franyeradriansanchez@gmail.com",
"homepage": "https://faslatam.42web.io", "homepage": "https://faslatam.42web.io/",
"role": "Maintainer" "role": "Maintainer"
}, },
{ {
@ -27,21 +27,16 @@
"ext-json": "*" "ext-json": "*"
}, },
"autoload": { "autoload": {
"files": [
"flight/autoload.php"
]
},
"autoload-dev": {
"classmap": [ "classmap": [
"tests/classes/" "src/Flight.php"
], ],
"psr-4": { "psr-4": {
"Tests\\PHP8\\": [ "flight\\": "src"
"tests/named-arguments" }
], },
"Tests\\Server\\": "tests/server", "autoload-dev": {
"Tests\\ServerV2\\": "tests/server-v2", "psr-4": {
"tests\\groupcompactsyntax\\": "tests/groupcompactsyntax" "tests\\": "tests"
} }
}, },
"require-dev": { "require-dev": {
@ -54,7 +49,9 @@
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^9.6",
"rregeer/phpunit-coverage-check": "^0.3.1", "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": { "config": {
"allow-plugins": { "allow-plugins": {
@ -65,15 +62,12 @@
}, },
"scripts": { "scripts": {
"test": "phpunit", "test": "phpunit",
"test-watcher": [ "test-watcher": "phpunit-watcher watch",
"phpunit-watcher || composer global require spatie/phpunit-watcher --dev",
"phpunit-watcher watch"
],
"test-coverage": [ "test-coverage": [
"rm -f clover.xml", "rm -rf coverage",
"@putenv XDEBUG_MODE=coverage", "@putenv XDEBUG_MODE=coverage",
"phpunit --coverage-html=coverage --coverage-clover=clover.xml", "phpunit --coverage-html coverage --coverage-clover coverage/clover.xml",
"coverage-check clover.xml 100" "coverage-check coverage/clover.xml 100"
], ],
"test-server": [ "test-server": [
"echo \"Running Test Server\"", "echo \"Running Test Server\"",
@ -81,12 +75,7 @@
], ],
"test-server-v2": [ "test-server-v2": [
"echo \"Running Test Server\"", "echo \"Running Test Server\"",
"@php -S localhost:8000 -t tests/server-v2" "@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"
], ],
"test-performance": [ "test-performance": [
"echo \"Running Performance Tests...\"", "echo \"Running Performance Tests...\"",
@ -97,13 +86,18 @@
"rm server.pid", "rm server.pid",
"echo \"Performance Tests Completed.\"" "echo \"Performance Tests Completed.\""
], ],
"lint": "phpstan --no-progress --memory-limit=256M", "lint": [
"beautify": "phpcbf", "phpstan --no-progress --memory-limit=256M",
"phpcs": "phpcs", "phpcs"
],
"format": [
"phpcbf"
],
"post-install-cmd": [ "post-install-cmd": [
"@php -r \"if (!file_exists('phpcs.xml')) copy('phpcs.xml.dist', 'phpcs.xml');\"", "@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('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": { "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"/> <exclude name="PSR12.Classes.AnonClassDeclaration.SpaceAfterKeyword"/>
</rule> </rule>
<file>index.php</file> <file>src</file>
<file>flight</file>
<file>tests</file> <file>tests</file>
</ruleset> </ruleset>

@ -5,6 +5,5 @@ includes:
parameters: parameters:
level: 6 level: 6
paths: paths:
- flight - src
- index.php
treatPhpDocTypesAsCertain: false 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"> colors="true">
<coverage processUncoveredFiles="false"> <coverage processUncoveredFiles="false">
<include> <include>
<directory suffix=".php">flight/</directory> <directory suffix=".php">src/</directory>
</include> </include>
<exclude> <exclude>
<file>flight/autoload.php</file> <file>src/autoload.php</file>
</exclude> </exclude>
</coverage> </coverage>
<testsuites> <testsuites>
<testsuite name="default"> <testsuite name="default">
<directory>tests/</directory> <directory>tests/</directory>
<exclude>tests/named-arguments/</exclude> <exclude>tests/named_arguments/</exclude>
</testsuite> </testsuite>
</testsuites> </testsuites>
<logging /> <logging />

@ -25,22 +25,23 @@ use Psr\Container\ContainerInterface;
* and generating an HTTP response. * and generating an HTTP response.
* *
* @license MIT, https://docs.flightphp.com/license * @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 start()
* @method void stop() * @method void stop(?int $code = null)
* @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true)
* @method EventDispatcher eventDispatcher() * @method EventDispatcher eventDispatcher()
* @method Route route(string $pattern, callable|string|array $callback, bool $pass_route = false, string $alias = '') * @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 $group_middlewares = []) * @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 $callback, bool $pass_route = false, string $alias = '') * @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 $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 $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 $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, string $controllerClass, array $methods = []) * @method void resource(string $pattern, class-string $controllerClass, array<string, string|array<int, string>> $methods = [])
* @method Router router() * @method Router router()
* @method string getUrl(string $alias) * @method string getUrl(string $alias, array<string, mixed> $params)
* @method void render(string $file, array $data = null, string $key = null) * @method void render(string $file, ?array<string, mixed> $data, string $key = null)
* @method View view() * @method View view()
* @method void onEvent(string $event, callable $callback) * @method void onEvent(string $event, callable $callback)
* @method void triggerEvent(string $event, ...$args) * @method void triggerEvent(string $event, ...$args)
@ -56,35 +57,10 @@ use Psr\Container\ContainerInterface;
* @method void etag(string $id, string $type = 'strong') * @method void etag(string $id, string $type = 'strong')
* @method void lastModified(int $time) * @method void lastModified(int $time)
* @method void download(string $filePath) * @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 class Engine
{ {
/** /** @var array<int, string> List of methods that can be extended in the Engine class */
* @var array<string> List of methods that can be extended in the Engine class.
*/
private const MAPPABLE_METHODS = [ private const MAPPABLE_METHODS = [
'start', 'start',
'stop', 'stop',
@ -112,53 +88,76 @@ class Engine
'triggerEvent' 'triggerEvent'
]; ];
/** @var array<string, mixed> Stored variables. */ /** @var array<string, mixed> */
protected array $vars = []; 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; protected Loader $loader;
/** @var Dispatcher<EngineTemplate> Method and class dispatcher. */
protected Dispatcher $dispatcher; protected Dispatcher $dispatcher;
/** Event dispatcher. */
protected EventDispatcher $eventDispatcher; protected EventDispatcher $eventDispatcher;
/** If the framework has been initialized or not. */ /** If the request has been handled or not */
protected bool $initialized = false; private bool $requestHandled = false;
/** If the request has been handled or not. */
protected bool $requestHandled = false;
public function __construct() public function __construct()
{ {
$this->loader = new Loader(); $this->loader = new Loader();
$this->dispatcher = new Dispatcher(); $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 string $name Method name
* @param array<int, mixed> $params Method parameters * @param array<int, mixed> $arguments Method parameters
* * @throws Throwable
* @throws Exception * @return mixed
* @return mixed Callback results
*/ */
public function __call(string $name, array $params) public function __call(string $name, array $arguments)
{ {
$callback = $this->dispatcher->get($name); $callback = $this->dispatcher->get($name);
if (\is_callable($callback)) { if (is_callable($callback)) {
return $this->dispatcher->run($name, $params); return $this->dispatcher->run($name, $arguments);
} }
if (!$this->loader->get($name)) { if (!$this->loader->get($name)) {
throw new Exception("$name must be a mapped method."); 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); return $this->loader->load($name, $shared);
} }
@ -167,79 +166,14 @@ class Engine
// Core Methods // // 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. * Converts errors into exceptions
* * @param int $errno Level of the error raised.
* @param int $errno Error number * @param string $errstr Error message.
* @param string $errstr Error string * @param string $errfile Filename that the error was raised in.
* @param string $errfile Error file name * @param int $errline Line number where the error was raised.
* @param int $errline Error file line number
*
* @return false
* @throws ErrorException * @throws ErrorException
* @return false
*/ */
public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
{ {
@ -250,27 +184,21 @@ class Engine
return false; return false;
} }
/** /** Logs exceptions */
* Custom exception handler. Logs exceptions. public function handleException(Throwable $ex): void
*
* @param Throwable $e Thrown exception
*/
public function handleException(Throwable $e): void
{ {
if ($this->get('flight.log_errors')) { 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 * 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 * @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 public function registerContainerHandler($containerHandler): void
{ {
@ -278,25 +206,26 @@ class Engine
} }
/** /**
* Maps a callback to a framework method. * Maps a callback to a framework method
*
* @param string $name Method name
* @param callable $callback Callback function
*
* @throws Exception If trying to map over a framework method * @throws Exception If trying to map over a framework method
*/ */
public function map(string $name, callable $callback): void 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.'); throw new Exception('Cannot override an existing framework method.');
} }
$this->dispatcher->set($name, $callback); return $this;
} }
/** /**
* Registers a class to a framework method. * Registers a class to a framework method.
*
* # Usage example: * # Usage example:
* ``` * ```
* $app = new Engine; * $app = new Engine;
@ -304,57 +233,47 @@ class Engine
* *
* $app->user(); # <- Return a User instance * $app->user(); # <- Return a User instance
* ``` * ```
* * @template T of object
* @param string $name Method name * @param string $name Method name
* @param class-string<T> $class Class name * @param class-string<T> $class Class name
* @param array<int, mixed> $params Class initialization parameters * @param array<int, mixed> $params Class initialization parameters
* @param ?Closure(T $instance): void $callback Function to call after object instantiation * @param ?callable(T): void $callback Function to call after object instantiation
*
* @template T of object
* @throws Exception If trying to map over a framework method * @throws Exception If trying to map over a framework method
*/ */
public function register(string $name, string $class, array $params = [], ?callable $callback = null): void public function register(string $name, string $class, array $params = [], ?callable $callback = null): void
{ {
if (method_exists($this, $name)) { $this->ensureMethodNotExists($name)->loader->register($name, $class, $params, $callback);
throw new Exception('Cannot override an existing framework method.');
}
$this->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 public function unregister(string $methodName): void
{ {
$this->loader->unregister($methodName); $this->loader->unregister($methodName);
} }
/** /**
* Adds a pre-filter to a method. * Adds a pre-filter to a method
*
* @param string $name Method name * @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 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 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 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 * @param ?string $key Variable name
*
* @return mixed Variable value or `null` if `$key` doesn't exists. * @return mixed Variable value or `null` if `$key` doesn't exists.
*/ */
public function get(?string $key = null) public function get(?string $key = null)
@ -367,15 +286,14 @@ class Engine
} }
/** /**
* Sets a variable. * Sets a variable
*
* @param string|iterable<string, mixed> $key * @param string|iterable<string, mixed> $key
* Variable name as `string` or an iterable of `'varName' => $varValue` * 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 public function set($key, $value = null): void
{ {
if (\is_iterable($key)) { if (is_iterable($key)) {
foreach ($key as $k => $v) { foreach ($key as $k => $v) {
$this->vars[$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 * @param string $key Variable name
*
* @return bool Variable status * @return bool Variable status
*/ */
public function has(string $key): bool 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. * @param ?string $key Variable name, if `$key` isn't provided, it clear all variables.
*/ */
public function clear(?string $key = null): void public function clear(?string $key = null): void
@ -414,22 +329,11 @@ class Engine
} }
/** /**
* Adds a path for class autoloading. * Processes each routes middleware
*
* @param string $dir Directory path
*/
public function path(string $dir): void
{
$this->loader->addDirectory($dir);
}
/**
* Processes each routes middleware.
*
* @param Route $route The route to process the middleware for. * @param Route $route The route to process the middleware for.
* @param string $eventName If this is the before or after method. * @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; $atLeastOneMiddlewareFailed = false;
@ -437,6 +341,7 @@ class Engine
$middlewares = $eventName === Dispatcher::FILTER_BEFORE $middlewares = $eventName === Dispatcher::FILTER_BEFORE
? $route->middleware ? $route->middleware
: array_reverse($route->middleware); : array_reverse($route->middleware);
$params = $route->params; $params = $route->params;
foreach ($middlewares as $middleware) { foreach ($middlewares as $middleware) {
@ -444,23 +349,23 @@ class Engine
$middlewareObject = false; $middlewareObject = false;
// Closure functions can only run on the before event // 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; $middlewareObject = $middleware;
// If the object has already been created, we can just use it if the event name exists. // If the object has already been created, we can just use it if the event name exists.
} elseif (is_object($middleware) === true) { } elseif (is_object($middleware)) {
$middlewareObject = method_exists($middleware, $eventName) === true ? [$middleware, $eventName] : false; $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. // 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; $resolvedClass = null;
// if there's a container assigned, we should use it to create the object // 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); $resolvedClass = $this->dispatcher->resolveContainerClass($middleware, $params);
// otherwise just assume it's a plain jane class, so inject the engine // otherwise just assume it's a plain jane class, so inject the engine
// just like in Dispatcher::invokeCallable() // just like in Dispatcher::invokeCallable()
} elseif (class_exists($middleware) === true) { } elseif (class_exists($middleware)) {
$resolvedClass = new $middleware($this); $resolvedClass = new $middleware($this);
} }
@ -471,16 +376,14 @@ class Engine
} }
// If nothing was resolved, go to the next thing // If nothing was resolved, go to the next thing
if ($middlewareObject === false) { if (!$middlewareObject) {
continue; continue;
} }
// This is the way that v3 handles output buffering (which captures output correctly) // This is the way that v3 handles output buffering (which captures output correctly)
$useV3OutputBuffering = $useV3OutputBuffering = !$route->is_streamed;
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
if ($useV3OutputBuffering === true) { if ($useV3OutputBuffering) {
ob_start(); ob_start();
} }
@ -498,7 +401,7 @@ class Engine
microtime(true) - $start microtime(true) - $start
); );
if ($useV3OutputBuffering === true) { if ($useV3OutputBuffering) {
$this->response()->write(ob_get_clean()); $this->response()->write(ob_get_clean());
} }
@ -550,17 +453,6 @@ class Engine
$response = $this->response(); $response = $this->response();
$router = $this->router(); $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 // Route the request
$failedMiddlewareCheck = false; $failedMiddlewareCheck = false;
while ($route = $router->route($request)) { while ($route = $router->route($request)) {
@ -610,22 +502,18 @@ class Engine
$this->triggerEvent('flight.middleware.before', $route); $this->triggerEvent('flight.middleware.before', $route);
} }
$useV3OutputBuffering = $useV3OutputBuffering = !$route->is_streamed;
$this->response()->v2_output_buffering === false &&
$route->is_streamed === false;
if ($useV3OutputBuffering === true) { if ($useV3OutputBuffering) {
ob_start(); ob_start();
} }
// Call route handler // Call route handler
$routeStart = microtime(true); $routeStart = microtime(true);
$continue = $this->dispatcher->execute( $continue = $this->dispatcher->execute($route->callback, $params);
$route->callback,
$params
);
$this->triggerEvent('flight.route.executed', $route, microtime(true) - $routeStart); $this->triggerEvent('flight.route.executed', $route, microtime(true) - $routeStart);
if ($useV3OutputBuffering === true) {
if ($useV3OutputBuffering) {
$response->write(ob_get_clean()); $response->write(ob_get_clean());
} }
@ -638,6 +526,7 @@ class Engine
$failedMiddlewareCheck = true; $failedMiddlewareCheck = true;
break; break;
} }
$this->triggerEvent('flight.middleware.after', $route); $this->triggerEvent('flight.middleware.after', $route);
} }
@ -648,7 +537,6 @@ class Engine
} }
$router->next(); $router->next();
$dispatched = false; $dispatched = false;
} }
@ -662,7 +550,7 @@ class Engine
} elseif ($dispatched === false) { } elseif ($dispatched === false) {
// Get the previous route and check if the method failed, but the URL was good. // Get the previous route and check if the method failed, but the URL was good.
$lastRouteExecuted = $router->executedRoute; $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); $this->methodNotFound($lastRouteExecuted);
} else { } else {
$this->notFound(); $this->notFound();
@ -720,10 +608,6 @@ class Engine
$response->status($code); $response->status($code);
} }
if ($response->v2_output_buffering === true && ob_get_length() > 0) {
$response->write(ob_get_clean());
}
$response->send(); $response->send();
} }
} }
@ -948,9 +832,6 @@ class Engine
->status($code) ->status($code)
->header('Content-Type', 'application/json') ->header('Content-Type', 'application/json')
->write($json); ->write($json);
if ($this->response()->v2_output_buffering === true) {
$this->response()->send();
}
} }
/** /**
@ -973,10 +854,8 @@ class Engine
): void { ): void {
$this->json($data, $code, $encode, $charset, $option); $this->json($data, $code, $encode, $charset, $option);
$jsonBody = $this->response()->getBody(); $jsonBody = $this->response()->getBody();
if ($this->response()->v2_output_buffering === false) { $this->response()->clearBody();
$this->response()->clearBody(); $this->response()->send();
$this->response()->send();
}
$this->halt($code, $jsonBody, empty(getenv('PHPUNIT_TEST'))); $this->halt($code, $jsonBody, empty(getenv('PHPUNIT_TEST')));
} }
@ -1007,9 +886,6 @@ class Engine
->status($code) ->status($code)
->header('Content-Type', 'application/javascript; charset=' . $charset) ->header('Content-Type', 'application/javascript; charset=' . $charset)
->write($callback . '(' . $json . ');'); ->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 * allows you to hook other functions to an event that can modify the
* input parameters and/or the output. * 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> * @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @phpstan-template EngineTemplate of object
*/ */
class Dispatcher class Dispatcher
{ {
@ -30,7 +29,6 @@ class Dispatcher
/** Exception message if thrown by setting the container as a callable method. */ /** Exception message if thrown by setting the container as a callable method. */
protected ?Throwable $containerException = null; protected ?Throwable $containerException = null;
/** @var ?Engine<EngineTemplate> $engine Engine instance. */
protected ?Engine $engine = null; protected ?Engine $engine = null;
/** @var array<string, callable(): (void|mixed)> Mapped events. */ /** @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 public function setEngine(Engine $engine): void
{ {
$this->engine = $engine; $this->engine = $engine;
@ -160,14 +151,7 @@ class Dispatcher
return $output; return $output;
} }
/** /** Assigns a callback to an event */
* Assigns a callback to an event.
*
* @param string $name Event name.
* @param callable(): (void|mixed) $callback Callback function.
*
* @return $this
*/
public function set(string $name, callable $callback): self public function set(string $name, callable $callback): self
{ {
$this->events[$name] = $callback; $this->events[$name] = $callback;
@ -175,13 +159,7 @@ class Dispatcher
return $this; return $this;
} }
/** /** Gets an assigned callback */
* Gets an assigned callback.
*
* @param string $name Event name.
*
* @return null|(callable(): (void|mixed)) $callback Callback function.
*/
public function get(string $name): ?callable public function get(string $name): ?callable
{ {
return $this->events[$name] ?? null; return $this->events[$name] ?? null;
@ -500,11 +478,7 @@ class Dispatcher
} }
} }
/** /** Resets the object to the initial state */
* Resets the object to the initial state.
*
* @return $this
*/
public function reset(): self public function reset(): self
{ {
$this->events = []; $this->events = [];

@ -6,51 +6,44 @@ namespace flight\core;
class EventDispatcher class EventDispatcher
{ {
/** @var self|null Singleton instance of the EventDispatcher */ /** Singleton instance of the EventDispatcher */
private static ?self $instance = null; private static ?self $instance = null;
/** @var array<string, array<int, callable>> */ /** @var array<string, array<int, callable(): mixed>> */
protected array $listeners = []; protected array $listeners = [];
/** /** Singleton instance of the EventDispatcher */
* Singleton instance of the EventDispatcher.
*
* @return self
*/
public static function getInstance(): self public static function getInstance(): self
{ {
if (self::$instance === null) { if (!self::$instance) {
self::$instance = new self(); self::$instance = new self();
} }
return self::$instance; return self::$instance;
} }
/** /**
* Register a callback for an event. * Register a callback for an event.
*
* @param string $event Event name * @param string $event Event name
* @param callable $callback Callback function * @param callable(): mixed $callback Callback function
*/ */
public function on(string $event, callable $callback): void public function on(string $event, callable $callback): void
{ {
if (isset($this->listeners[$event]) === false) { $this->listeners[$event] ??= [];
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $callback; $this->listeners[$event][] = $callback;
} }
/** /**
* Trigger an event with optional arguments. * Trigger an event with optional arguments.
*
* @param string $event Event name * @param string $event Event name
* @param mixed ...$args Arguments to pass to the callbacks * @param mixed ...$args Arguments to pass to the callbacks
*
* @return mixed * @return mixed
*/ */
public function trigger(string $event, ...$args) public function trigger(string $event, ...$args)
{ {
$result = null; $result = null;
if (isset($this->listeners[$event]) === true) {
if (isset($this->listeners[$event])) {
foreach ($this->listeners[$event] as $callback) { foreach ($this->listeners[$event] as $callback) {
$result = call_user_func_array($callback, $args); $result = call_user_func_array($callback, $args);
@ -60,27 +53,24 @@ class EventDispatcher
} }
} }
} }
return $result; return $result;
} }
/** /**
* Check if an event has any registered listeners. * Check if an event has any registered listeners.
*
* @param string $event Event name * @param string $event Event name
*
* @return bool True if the event has listeners, false otherwise * @return bool True if the event has listeners, false otherwise
*/ */
public function hasListeners(string $event): bool 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. * Get all listeners registered for a specific event.
*
* @param string $event Event name * @param string $event Event name
* * @return array<int, callable(): mixed> Array of callbacks registered for the event
* @return array<int, callable> Array of callbacks registered for the event
*/ */
public function getListeners(string $event): array public function getListeners(string $event): array
{ {
@ -89,7 +79,6 @@ class EventDispatcher
/** /**
* Get a list of all events that have registered listeners. * Get a list of all events that have registered listeners.
*
* @return array<int, string> Array of event names * @return array<int, string> Array of event names
*/ */
public function getAllRegisteredEvents(): array public function getAllRegisteredEvents(): array
@ -99,41 +88,31 @@ class EventDispatcher
/** /**
* Remove a specific listener for an event. * Remove a specific listener for an event.
*
* @param string $event the event name * @param string $event the event name
* @param callable $callback the exact callback to remove * @param callable(): mixed $callback the exact callback to remove
*
* @return void
*/ */
public function removeListener(string $event, callable $callback): void public function removeListener(string $event, callable $callback): void
{ {
if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) { if ($this->hasListeners($event)) {
$this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) { $this->listeners[$event] = array_filter(
return $listener !== $callback; $this->listeners[$event],
}); static fn(callable $listener): bool => $listener !== $callback,
);
$this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array $this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array
} }
} }
/** /**
* Remove all listeners for a specific event. * Remove all listeners for a specific event.
*
* @param string $event the event name * @param string $event the event name
*
* @return void
*/ */
public function removeAllListeners(string $event): 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. */
* Remove the current singleton instance of the EventDispatcher.
*
* @return void
*/
public static function resetInstance(): void public static function resetInstance(): void
{ {
self::$instance = null; 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; 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 * HTTP status codes
* *
@ -271,7 +262,7 @@ class Response
$this->clearBody(); $this->clearBody();
// This needs to clear the output buffer if it's on // 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(); ob_clean();
} }
@ -421,18 +412,8 @@ class Response
*/ */
public function send(): void 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); $start = microtime(true);
// Only for the v3 output buffering. $this->processResponseCallbacks();
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
}
if ($this->headersSent() === false) { if ($this->headersSent() === false) {
$this->sendHeaders(); $this->sendHeaders();

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

@ -16,7 +16,7 @@ class DocExamplesTest extends TestCase
{ {
$_SERVER = []; $_SERVER = [];
$_REQUEST = []; $_REQUEST = [];
Flight::init(); Flight::app();
Flight::setEngine(new Engine()); Flight::setEngine(new Engine());
} }
@ -45,25 +45,6 @@ class DocExamplesTest extends TestCase
$this->assertEquals('[]', Flight::response()->getBody()); $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 public function testMapErrorMethod(): void
{ {
Flight::map('error', function (Throwable $error) { Flight::map('error', function (Throwable $error) {
@ -81,12 +62,9 @@ class DocExamplesTest extends TestCase
Flight::request()->method = 'GET'; Flight::request()->method = 'GET';
Flight::request()->url = '/'; Flight::request()->url = '/';
$router->get( $router->get('/', static function () {
'/', Flight::response()->write('from resp ');
function () { });
Flight::response()->write('from resp ');
}
);
Flight::start(); Flight::start();

@ -32,13 +32,7 @@ class EngineTest extends TestCase
public function testInitBeforeStart(): void public function testInitBeforeStart(): void
{ {
$engine = new class extends Engine { $engine = new Engine;
public function getInitializedVar()
{
return $this->initialized;
}
};
$this->assertTrue($engine->getInitializedVar());
// we need to setup a dummy route // we need to setup a dummy route
$engine->route('/someRoute', function () {}); $engine->route('/someRoute', function () {});
@ -49,25 +43,6 @@ class EngineTest extends TestCase
$this->assertTrue($engine->response()->content_length); $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 public function testHandleErrorNoErrorNumber(): void
{ {
$engine = new Engine(); $engine = new Engine();
@ -153,26 +128,6 @@ class EngineTest extends TestCase
$engine->start(); $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 public function testDoubleReturnTrueRoutesContinueIteration(): void
{ {
$_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_METHOD'] = 'GET';
@ -322,34 +277,6 @@ class EngineTest extends TestCase
$this->assertEquals(500, $engine->response()->status()); $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 public function testPostRoute(): void
{ {
$engine = new Engine(); $engine = new Engine();
@ -519,16 +446,6 @@ class EngineTest extends TestCase
$engine->json(['key1' => 'value1', 'key2' => 'value2', 'utf8_emoji' => "\xB1\x31"]); $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 public function testJsonHalt(): void
{ {
$engine = new Engine(); $engine = new Engine();
@ -549,17 +466,6 @@ class EngineTest extends TestCase
$this->assertEquals('whatever({"key1":"value1","key2":"value2"});', $engine->response()->getBody()); $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 public function testJsonpBadParam(): void
{ {
$engine = new Engine(); $engine = new Engine();
@ -569,16 +475,6 @@ class EngineTest extends TestCase
$this->assertEquals(200, $engine->response()->status()); $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 public function testEtagSimple(): void
{ {
$engine = new Engine(); $engine = new Engine();

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

@ -20,7 +20,7 @@ class FlightTest extends TestCase
{ {
$_SERVER = []; $_SERVER = [];
$_REQUEST = []; $_REQUEST = [];
Flight::init(); Flight::app();
Flight::setEngine(new Engine()); Flight::setEngine(new Engine());
Flight::set('flight.views.path', __DIR__ . '/views'); Flight::set('flight.views.path', __DIR__ . '/views');
} }
@ -71,8 +71,6 @@ class FlightTest extends TestCase
// Register a class // Register a class
public function testRegister(): void public function testRegister(): void
{ {
Flight::path(__DIR__ . '/classes');
Flight::register('user', User::class); Flight::register('user', User::class);
$user = Flight::user(); $user = Flight::user();
@ -277,25 +275,6 @@ class FlightTest extends TestCase
$this->assertEquals('test', Flight::response()->getBody()); $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 public function testStreamRoute(): void
{ {
$response_mock = new class extends Response { $response_mock = new class extends Response {

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace tests; namespace tests;
use flight\core\Loader; use flight\core\Loader;
use tests\classes\Factory;
use tests\classes\User;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use tests\classes\User;
use tests\classes\Factory;
use tests\classes\TesterClass; use tests\classes\TesterClass;
class LoaderTest extends TestCase class LoaderTest extends TestCase
@ -17,63 +17,41 @@ class LoaderTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->loader = new Loader(); $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 public function testRegister(): void
{ {
$this->loader->register('a', User::class); $this->loader->register('a', User::class);
$user = $this->loader->load('a'); $user = $this->loader->load('a');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertEquals('', $user->name); self::assertSame('', $user->name);
} }
// Register a class with constructor parameters
public function testRegisterWithConstructor(): void public function testRegisterWithConstructor(): void
{ {
$this->loader->register('b', User::class, ['Bob']); $this->loader->register('b', User::class, ['Bob']);
$user = $this->loader->load('b'); $user = $this->loader->load('b');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $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 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->name = 'Fred';
}); });
$user = $this->loader->load('c'); $user = $this->loader->load('c');
self::assertIsObject($user);
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertEquals('Fred', $user->name); self::assertEquals('Fred', $user->name);
} }
// Get a non-shared instance of a class
public function testSharedInstance(): void public function testSharedInstance(): void
{ {
$this->loader->register('d', User::class); $this->loader->register('d', User::class);
$user1 = $this->loader->load('d'); $user1 = $this->loader->load('d');
$user2 = $this->loader->load('d'); $user2 = $this->loader->load('d');
$user3 = $this->loader->load('d', false); $user3 = $this->loader->load('d', false);
@ -82,38 +60,24 @@ class LoaderTest extends TestCase
self::assertNotSame($user1, $user3); self::assertNotSame($user1, $user3);
} }
// Gets an object from a factory method
public function testRegisterUsingCallable(): void public function testRegisterUsingCallable(): void
{ {
$this->loader->register('e', ['\tests\classes\Factory', 'create']); $this->loader->register('e', [Factory::class, 'create']);
$obj = $this->loader->load('e'); $obj = $this->loader->load('e');
self::assertIsObject($obj);
self::assertInstanceOf(Factory::class, $obj);
$obj2 = $this->loader->load('e'); $obj2 = $this->loader->load('e');
$obj3 = $this->loader->load('e', false);
self::assertIsObject($obj2); self::assertInstanceOf(Factory::class, $obj);
self::assertInstanceOf(Factory::class, $obj2);
self::assertSame($obj, $obj2); self::assertSame($obj, $obj2);
$obj3 = $this->loader->load('e', false);
self::assertIsObject($obj3);
self::assertInstanceOf(Factory::class, $obj3); self::assertInstanceOf(Factory::class, $obj3);
self::assertNotSame($obj, $obj3); self::assertNotSame($obj, $obj3);
} }
// Gets an object from a callback function
public function testRegisterUsingCallback(): void public function testRegisterUsingCallback(): void
{ {
$this->loader->register('f', function () { $this->loader->register('f', static fn(): Factory => Factory::create());
return Factory::create();
});
$obj = $this->loader->load('f'); $obj = $this->loader->load('f');
self::assertIsObject($obj);
self::assertInstanceOf(Factory::class, $obj); self::assertInstanceOf(Factory::class, $obj);
} }
@ -121,15 +85,22 @@ class LoaderTest extends TestCase
{ {
$this->loader->register('g', User::class); $this->loader->register('g', User::class);
$current_class = $this->loader->get('g'); $current_class = $this->loader->get('g');
$this->assertEquals([User::class, [], null], $current_class);
$this->assertSame([User::class, [], null], $current_class);
$this->loader->unregister('g'); $this->loader->unregister('g');
$unregistered_class_result = $this->loader->get('g'); $unregistered_class_result = $this->loader->get('g');
$this->assertNull($unregistered_class_result); $this->assertNull($unregistered_class_result);
} }
public function testNewInstance6Params(): void 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('Bob', $TesterClass->param1);
$this->assertEquals('Fred', $TesterClass->param2); $this->assertEquals('Fred', $TesterClass->param2);
$this->assertEquals('Joe', $TesterClass->param3); $this->assertEquals('Joe', $TesterClass->param3);
@ -137,32 +108,4 @@ class LoaderTest extends TestCase
$this->assertEquals('Sally', $TesterClass->param5); $this->assertEquals('Sally', $TesterClass->param5);
$this->assertEquals('Suzie', $TesterClass->param6); $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 public function testRenderView(): void
{ {
$this->app->render('hello', ['name' => 'Bob']); $this->app->render('hello', ['name' => 'Bob']);
$this->expectOutputString('Hello, Bob!'); $this->expectOutputString('Hello, Bob!');
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -7,10 +7,10 @@ use flight\core\Loader;
use flight\database\PdoWrapper; use flight\database\PdoWrapper;
use tests\classes\Container; use tests\classes\Container;
use tests\classes\ContainerDefault; use tests\classes\ContainerDefault;
use Tests\Server\AuthCheck; use tests\server\AuthCheck;
use Tests\Server\LayoutMiddleware; use tests\server\LayoutMiddleware;
use Tests\Server\OverwriteBodyMiddleware; use tests\server\OverwriteBodyMiddleware;
use Tests\Server\Pascal_Snake_Case; use tests\server\Pascal_Snake_Case;
/* /*
* This is the test file where we can open up a quick test server and make * 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 * @author Kristaps Muižnieks https://github.com/krmu
*/ */
namespace Tests\ServerV2 { namespace tests\server_v2 {
class AuthCheck class AuthCheck
{ {
public function before(): void public function before(): void
@ -23,7 +23,7 @@ namespace Tests\ServerV2 {
namespace { namespace {
use Tests\ServerV2\AuthCheck; use tests\server_v2\AuthCheck;
require_once __DIR__ . '/../phpunit_autoload.php'; require_once __DIR__ . '/../phpunit_autoload.php';
Loading…
Cancel
Save