Merge pull request #501 from flightphp/master

Merging all new changes with OG framework
pull/504/head v3.0.0
n0nag0n 1 year ago committed by GitHub
commit 2df12f963e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,12 @@
root = true
[*]
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

5
.gitignore vendored

@ -2,3 +2,8 @@
vendor/ vendor/
composer.phar composer.phar
composer.lock composer.lock
.phpunit.result.cache
coverage/
.vscode/settings.json
*.sublime-workspace
.vscode/

@ -1,10 +1,25 @@
![](https://user-images.githubusercontent.com/104888/50957476-9c4acb80-14be-11e9-88ce-6447364dc1bb.png)
![](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat)
![](https://img.shields.io/matrix/flight-php-framework%3Amatrix.org?server_fqdn=matrix.org&style=social&logo=matrix)
[![HitCount](https://hits.dwyl.com/flightphp/core.svg?style=flat-square&show=unique)](http://hits.dwyl.com/flightphp/core)
# What the fork?
This is a fork of the original project [https://github.com/mikecao/flight](https://github.com/mikecao/flight). That project hasn't seen updates in quite some time, so this fork is to help maintain the project going forward.
# What is Flight? # What is Flight?
Flight is a fast, simple, extensible framework for PHP. Flight enables you to Flight is a fast, simple, extensible framework for PHP. Flight enables you to
quickly and easily build RESTful web applications. quickly and easily build RESTful web applications.
Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/#/#flight-php-framework:matrix.org)
# Basic Usage
```php ```php
require 'flight/Flight.php';
// if installed with composer
require 'vendor/autoload.php';
// or if installed manually by zip file
//require 'flight/Flight.php';
Flight::route('/', function() { Flight::route('/', function() {
echo 'hello world!'; echo 'hello world!';
@ -27,14 +42,15 @@ Flight is released under the [MIT](http://flightphp.com/license) license.
1\. Download the files. 1\. Download the files.
If you're using [Composer](https://getcomposer.org/), you can run the following command: If you're using [Composer](https://getcomposer.org/), you can run the following
command:
``` ```bash
composer require mikecao/flight composer require n0nag0n/flight
``` ```
OR you can [download](https://github.com/mikecao/flight/archive/master.zip) them directly OR you can [download](https://github.com/n0nag0n/flight/archive/master.zip)
and extract them to your web directory. them directly and extract them to your web directory.
2\. Configure your webserver. 2\. Configure your webserver.
@ -47,7 +63,8 @@ RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L] RewriteRule ^(.*)$ index.php [QSA,L]
``` ```
**Note**: If you need to use flight in a subdirectory add the line `RewriteBase /subdir/` just after `RewriteEngine On`. **Note**: If you need to use flight in a subdirectory add the line
`RewriteBase /subdir/` just after `RewriteEngine On`.
For *Nginx*, add the following to your server declaration: For *Nginx*, add the following to your server declaration:
@ -179,7 +196,7 @@ You can specify named parameters in your routes which will be passed along to
your callback function. your callback function.
```php ```php
Flight::route('/@name/@id', function($name, $id){ Flight::route('/@name/@id', function(string $name, string $id) {
echo "hello, $name ($id)!"; echo "hello, $name ($id)!";
}); });
``` ```
@ -188,7 +205,7 @@ You can also include regular expressions with your named parameters by using
the `:` delimiter: the `:` delimiter:
```php ```php
Flight::route('/@name/@id:[0-9]{3}', function($name, $id){ Flight::route('/@name/@id:[0-9]{3}', function(string $name, string $id) {
// This will match /bob/123 // This will match /bob/123
// But will not match /bob/12345 // But will not match /bob/12345
}); });
@ -202,13 +219,16 @@ You can specify named parameters that are optional for matching by wrapping
segments in parentheses. segments in parentheses.
```php ```php
Flight::route('/blog(/@year(/@month(/@day)))', function($year, $month, $day){ Flight::route(
'/blog(/@year(/@month(/@day)))',
function(?string $year, ?string $month, ?string $day) {
// This will match the following URLS: // This will match the following URLS:
// /blog/2012/12/10 // /blog/2012/12/10
// /blog/2012/12 // /blog/2012/12
// /blog/2012 // /blog/2012
// /blog // /blog
}); }
);
``` ```
Any optional parameters that are not matched will be passed in as NULL. Any optional parameters that are not matched will be passed in as NULL.
@ -238,7 +258,7 @@ You can pass execution on to the next matching route by returning `true` from
your callback function. your callback function.
```php ```php
Flight::route('/user/@name', function($name){ Flight::route('/user/@name', function (string $name) {
// Check some condition // Check some condition
if ($name != "Bob") { if ($name != "Bob") {
// Continue to next route // Continue to next route
@ -259,7 +279,7 @@ the route method. The route object will always be the last parameter passed to y
callback function. callback function.
```php ```php
Flight::route('/', function($route){ Flight::route('/', function(\flight\net\Route $route) {
// Array of HTTP methods matched against // Array of HTTP methods matched against
$route->methods; $route->methods;
@ -329,7 +349,7 @@ new object. The callback function takes one parameter, an instance of the new ob
```php ```php
// The callback will be passed the object that was constructed // The callback will be passed the object that was constructed
Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function($db){ Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function (PDO $db): void {
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}); });
``` ```
@ -387,7 +407,7 @@ methods as well as any custom methods that you've mapped.
A filter function looks like this: A filter function looks like this:
```php ```php
function(&$params, &$output) { function (array &$params, string &$output) {
// Filter code // Filter code
} }
``` ```
@ -397,7 +417,7 @@ Using the passed in variables you can manipulate the input parameters and/or the
You can have a filter run before a method by doing: You can have a filter run before a method by doing:
```php ```php
Flight::before('start', function(&$params, &$output){ Flight::before('start', function (array &$params, string &$output) {
// Do something // Do something
}); });
``` ```
@ -405,7 +425,7 @@ Flight::before('start', function(&$params, &$output){
You can have a filter run after a method by doing: You can have a filter run after a method by doing:
```php ```php
Flight::after('start', function(&$params, &$output){ Flight::after('start', function (array &$params, string &$output) {
// Do something // Do something
}); });
``` ```
@ -422,13 +442,13 @@ Flight::map('hello', function($name){
}); });
// Add a before filter // Add a before filter
Flight::before('hello', function(&$params, &$output){ Flight::before('hello', function (array &$params, string &$output) {
// Manipulate the parameter // Manipulate the parameter
$params[0] = 'Fred'; $params[0] = 'Fred';
}); });
// Add an after filter // Add an after filter
Flight::after('hello', function(&$params, &$output){ Flight::after('hello', function (array &$params, string &$output) {
// Manipulate the output // Manipulate the output
$output .= " Have a nice day!"; $output .= " Have a nice day!";
}); });
@ -439,17 +459,19 @@ echo Flight::hello('Bob');
This should display: This should display:
```
Hello Fred! Have a nice day! Hello Fred! Have a nice day!
```
If you have defined multiple filters, you can break the chain by returning `false` If you have defined multiple filters, you can break the chain by returning `false`
in any of your filter functions: in any of your filter functions:
```php ```php
Flight::before('start', function(&$params, &$output){ Flight::before('start', function (array &$params, string &$output){
echo 'one'; echo 'one';
}); });
Flight::before('start', function(&$params, &$output){ Flight::before('start', function (array &$params, string &$output): bool {
echo 'two'; echo 'two';
// This will end the chain // This will end the chain
@ -457,7 +479,7 @@ Flight::before('start', function(&$params, &$output){
}); });
// This will not get called // This will not get called
Flight::before('start', function(&$params, &$output){ Flight::before('start', function (array &$params, string &$output){
echo 'three'; echo 'three';
}); });
``` ```
@ -515,12 +537,14 @@ be reference like a local variable. Template files are simply PHP files. If the
content of the `hello.php` template file is: content of the `hello.php` template file is:
```php ```php
Hello, '<?php echo $name; ?>'! Hello, <?php echo $name; ?>!
``` ```
The output would be: The output would be:
```
Hello, Bob! Hello, Bob!
```
You can also manually set view variables by using the set method: You can also manually set view variables by using the set method:
@ -615,11 +639,11 @@ require './Smarty/libs/Smarty.class.php';
// Register Smarty as the view class // Register Smarty as the view class
// Also pass a callback function to configure Smarty on load // Also pass a callback function to configure Smarty on load
Flight::register('view', 'Smarty', array(), function($smarty){ Flight::register('view', 'Smarty', array(), function (Smarty $smarty) {
$smarty->template_dir = './templates/'; $smarty->setTemplateDir() = './templates/';
$smarty->compile_dir = './templates_c/'; $smarty->setCompileDir() = './templates_c/';
$smarty->config_dir = './config/'; $smarty->setConfigDir() = './config/';
$smarty->cache_dir = './cache/'; $smarty->setCacheDir() = './cache/';
}); });
// Assign template data // Assign template data
@ -648,7 +672,7 @@ response with some error information.
You can override this behavior for your own needs: You can override this behavior for your own needs:
```php ```php
Flight::map('error', function(Exception $ex){ Flight::map('error', function(Throwable $ex){
// Handle error // Handle error
echo $ex->getTraceAsString(); echo $ex->getTraceAsString();
}); });
@ -701,26 +725,24 @@ $request = Flight::request();
The request object provides the following properties: The request object provides the following properties:
``` - **url** - The URL being requested
url - The URL being requested - **base** - The parent subdirectory of the URL
base - The parent subdirectory of the URL - **method** - The request method (GET, POST, PUT, DELETE)
method - The request method (GET, POST, PUT, DELETE) - **referrer** - The referrer URL
referrer - The referrer URL - **ip** - IP address of the client
ip - IP address of the client - **ajax** - Whether the request is an AJAX request
ajax - Whether the request is an AJAX request - **scheme** - The server protocol (http, https)
scheme - The server protocol (http, https) - **user_agent** - Browser information
user_agent - Browser information - **type** - The content type
type - The content type - **length** - The content length
length - The content length - **query** - Query string parameters
query - Query string parameters - **data** - Post data or JSON data
data - Post data or JSON data - **cookies** - Cookie data
cookies - Cookie data - **files** - Uploaded files
files - Uploaded files - **secure** - Whether the connection is secure
secure - Whether the connection is secure - **accept** - HTTP accept parameters
accept - HTTP accept parameters - **proxy_ip** - Proxy IP address of the client
proxy_ip - Proxy IP address of the client - **host** - The request host name
host - The request host name
```
You can access the `query`, `data`, `cookies`, and `files` properties You can access the `query`, `data`, `cookies`, and `files` properties
as arrays or objects. as arrays or objects.
@ -739,7 +761,8 @@ $id = Flight::request()->query->id;
## RAW Request Body ## RAW Request Body
To get the raw HTTP request body, for example when dealing with PUT requests, you can do: To get the raw HTTP request body, for example when dealing with PUT requests,
you can do:
```php ```php
$body = Flight::request()->getBody(); $body = Flight::request()->getBody();
@ -747,8 +770,8 @@ $body = Flight::request()->getBody();
## JSON Input ## JSON Input
If you send a request with the type `application/json` and the data `{"id": 123}` it will be available If you send a request with the type `application/json` and the data `{"id": 123}`
from the `data` property: it will be available from the `data` property:
```php ```php
$id = Flight::request()->data->id; $id = Flight::request()->data->id;
@ -847,12 +870,12 @@ Flight::set('flight.log_errors', true);
The following is a list of all the available configuration settings: The following is a list of all the available configuration settings:
flight.base_url - Override the base url of the request. (default: null) - **flight.base_url** - Override the base url of the request. (default: null)
flight.case_sensitive - Case sensitive matching for URLs. (default: false) - **flight.case_sensitive** - Case sensitive matching for URLs. (default: false)
flight.handle_errors - Allow Flight to handle all errors internally. (default: true) - **flight.handle_errors** - Allow Flight to handle all errors internally. (default: true)
flight.log_errors - Log errors to the web server's error log file. (default: false) - **flight.log_errors** - Log errors to the web server's error log file. (default: false)
flight.views.path - Directory containing view template files. (default: ./views) - **flight.views.path** - Directory containing view template files. (default: ./views)
flight.views.extension - View template file extension. (default: .php) - **flight.views.extension** - View template file extension. (default: .php)
# Framework Methods # Framework Methods
@ -864,15 +887,15 @@ or overridden.
## Core Methods ## Core Methods
```php ```php
Flight::map($name, $callback) // Creates a custom framework method. Flight::map(string $name, callable $callback, bool $pass_route = false) // Creates a custom framework method.
Flight::register($name, $class, [$params], [$callback]) // Registers a class to a framework method. Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registers a class to a framework method.
Flight::before($name, $callback) // Adds a filter before a framework method. Flight::before(string $name, callable $callback) // Adds a filter before a framework method.
Flight::after($name, $callback) // Adds a filter after a framework method. Flight::after(string $name, callable $callback) // Adds a filter after a framework method.
Flight::path($path) // Adds a path for autoloading classes. Flight::path(string $path) // Adds a path for autoloading classes.
Flight::get($key) // Gets a variable. Flight::get(string $key) // Gets a variable.
Flight::set($key, $value) // Sets a variable. Flight::set(string $key, mixed $value) // Sets a variable.
Flight::has($key) // Checks if a variable is set. Flight::has(string $key) // Checks if a variable is set.
Flight::clear([$key]) // Clears a variable. Flight::clear(array|string $key = []) // Clears a variable.
Flight::init() // Initializes the framework to its default settings. Flight::init() // Initializes the framework to its default settings.
Flight::app() // Gets the application object instance Flight::app() // Gets the application object instance
``` ```
@ -882,16 +905,16 @@ Flight::app() // Gets the application object instance
```php ```php
Flight::start() // Starts the framework. Flight::start() // Starts the framework.
Flight::stop() // Stops the framework and sends a response. Flight::stop() // Stops the framework and sends a response.
Flight::halt([$code], [$message]) // Stop the framework with an optional status code and message. Flight::halt(int $code = 200, string $message = '') // Stop the framework with an optional status code and message.
Flight::route($pattern, $callback) // Maps a URL pattern to a callback. Flight::route(string $pattern, callable $callback, bool $pass_route = false) // Maps a URL pattern to a callback.
Flight::redirect($url, [$code]) // Redirects to another URL. Flight::redirect(string $url, int $code) // Redirects to another URL.
Flight::render($file, [$data], [$key]) // Renders a template file. Flight::render(string $file, array $data, ?string $key = null) // Renders a template file.
Flight::error($exception) // Sends an HTTP 500 response. Flight::error(Throwable $exception) // Sends an HTTP 500 response.
Flight::notFound() // Sends an HTTP 404 response. Flight::notFound() // Sends an HTTP 404 response.
Flight::etag($id, [$type]) // Performs ETag HTTP caching. Flight::etag(string $id, string $type = 'string') // Performs ETag HTTP caching.
Flight::lastModified($time) // Performs last modified HTTP caching. Flight::lastModified(int $time) // Performs last modified HTTP caching.
Flight::json($data, [$code], [$encode], [$charset], [$option]) // Sends a JSON response. Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response.
Flight::jsonp($data, [$param], [$code], [$encode], [$charset], [$option]) // Sends a JSONP response. Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSONP response.
``` ```
Any custom methods added with `map` and `register` can also be filtered. Any custom methods added with `map` and `register` can also be filtered.
@ -905,9 +928,7 @@ as an object instance.
```php ```php
require 'flight/autoload.php'; require 'flight/autoload.php';
use flight\Engine; $app = new flight\Engine();
$app = new Engine();
$app->route('/', function () { $app->route('/', function () {
echo 'hello world!'; echo 'hello world!';

@ -1 +1 @@
2.0.1 3.0.0

@ -1,6 +1,6 @@
{ {
"name": "mikecao/flight", "name": "flightphp/core",
"description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications.", "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": "http://flightphp.com",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
@ -9,16 +9,52 @@
"email": "mike@mikecao.com", "email": "mike@mikecao.com",
"homepage": "http://www.mikecao.com/", "homepage": "http://www.mikecao.com/",
"role": "Original Developer" "role": "Original Developer"
},
{
"name": "Franyer Sánchez",
"email": "franyeradriansanchez@gmail.com",
"homepage": "https://faslatam.000webhostapp.com",
"role": "Maintainer"
},
{
"name": "n0nag0n",
"email": "n0nag0n@sky-9.com",
"role": "Maintainer"
} }
], ],
"require": { "require": {
"php": "^7.4|^8.0|^8.1", "php": "^7.4|^8.0|^8.1|^8.2",
"ext-json": "*" "ext-json": "*"
}, },
"autoload": { "autoload": {
"files": [ "flight/autoload.php", "flight/Flight.php" ] "files": [
"flight/autoload.php",
"flight/Flight.php"
]
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5" "phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.10",
"phpstan/extension-installer": "^1.3"
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
},
"scripts": {
"test": "phpunit",
"test-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage",
"lint": "phpstan --no-progress -cphpstan.neon"
},
"suggest": {
"phpstan/phpstan": "PHP Static Analysis Tool",
"latte/latte": "Latte template engine"
},
"suggest-dev": {
"tracy/tracy": "Tracy debugger"
},
"replace": {
"mikecao/flight": "2.0.2"
} }
} }

@ -0,0 +1,21 @@
{
"folders": [
{
"path": ".",
}
],
"settings": {
"LSP": {
"LSP-intelephense": {
"settings": {
"intelephense.environment.phpVersion": "7.4.0",
"intelephense.format.braces": "psr12",
},
},
"formatters":
{
"embedding.php": "LSP-intelephense"
},
},
},
}

@ -30,7 +30,14 @@ use Throwable;
* @method void start() Starts engine * @method void start() Starts engine
* @method void stop() Stops framework and outputs current response * @method void stop() Stops framework and outputs current response
* @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response. * @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response.
*
* Routing
* @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function. * @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function.
* @method void get(string $pattern, callable $callback, bool $pass_route = false) Routes a GET URL to a callback function.
* @method void post(string $pattern, callable $callback, bool $pass_route = false) Routes a POST URL to a callback function.
* @method void put(string $pattern, callable $callback, bool $pass_route = false) Routes a PUT URL to a callback function.
* @method void patch(string $pattern, callable $callback, bool $pass_route = false) Routes a PATCH URL to a callback function.
* @method void delete(string $pattern, callable $callback, bool $pass_route = false) Routes a DELETE URL to a callback function.
* @method Router router() Gets router * @method Router router() Gets router
* *
* Views * Views
@ -40,7 +47,7 @@ use Throwable;
* Request-response * Request-response
* @method Request request() Gets current request * @method Request request() Gets current request
* @method Response response() Gets current response * @method Response response() Gets current response
* @method void error(Exception $e) Sends an HTTP 500 response for any errors. * @method void error(Throwable $e) Sends an HTTP 500 response for any errors.
* @method void notFound() Sends an HTTP 404 response when a URL is not found. * @method void notFound() Sends an HTTP 404 response when a URL is not found.
* @method void redirect(string $url, int $code = 303) Redirects the current request to another URL. * @method void redirect(string $url, int $code = 303) Redirects the current request to another URL.
* @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) Sends a JSON response. * @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) Sends a JSON response.
@ -54,6 +61,7 @@ class Engine
{ {
/** /**
* Stored variables. * Stored variables.
* @var array<string, mixed>
*/ */
protected array $vars; protected array $vars;
@ -67,6 +75,13 @@ class Engine
*/ */
protected Dispatcher $dispatcher; protected Dispatcher $dispatcher;
/**
* If the framework has been initialized or not
*
* @var boolean
*/
protected bool $initialized = false;
/** /**
* Constructor. * Constructor.
*/ */
@ -84,7 +99,7 @@ class Engine
* Handles calls to class methods. * Handles calls to class methods.
* *
* @param string $name Method name * @param string $name Method name
* @param array $params Method parameters * @param array<int, mixed> $params Method parameters
* *
* @throws Exception * @throws Exception
* *
@ -114,7 +129,7 @@ class Engine
*/ */
public function init(): void public function init(): void
{ {
static $initialized = false; $initialized = $this->initialized;
$self = $this; $self = $this;
if ($initialized) { if ($initialized) {
@ -155,8 +170,8 @@ class Engine
$this->before('start', function () use ($self) { $this->before('start', function () use ($self) {
// Enable error handling // Enable error handling
if ($self->get('flight.handle_errors')) { if ($self->get('flight.handle_errors')) {
set_error_handler([$self, 'handleError']); set_error_handler(array($self, 'handleError'));
set_exception_handler([$self, 'handleException']); set_exception_handler(array($self, 'handleException'));
} }
// Set case-sensitivity // Set case-sensitivity
@ -165,7 +180,7 @@ class Engine
$self->response()->content_length = $self->get('flight.content_length'); $self->response()->content_length = $self->get('flight.content_length');
}); });
$initialized = true; $this->initialized = true;
} }
/** /**
@ -177,23 +192,26 @@ class Engine
* @param int $errline Error file line number * @param int $errline Error file line number
* *
* @throws ErrorException * @throws ErrorException
* @return bool
*/ */
public function handleError(int $errno, string $errstr, string $errfile, int $errline) public function handleError(int $errno, string $errstr, string $errfile, int $errline)
{ {
if ($errno & error_reporting()) { if ($errno & error_reporting()) {
throw new ErrorException($errstr, $errno, 0, $errfile, $errline); throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
} }
return false;
} }
/** /**
* Custom exception handler. Logs exceptions. * Custom exception handler. Logs exceptions.
* *
* @param Exception $e Thrown exception * @param Throwable $e Thrown exception
*/ */
public function handleException($e): void public function handleException($e): void
{ {
if ($this->get('flight.log_errors')) { if ($this->get('flight.log_errors')) {
error_log($e->getMessage()); error_log($e->getMessage()); // @codeCoverageIgnore
} }
$this->error($e); $this->error($e);
@ -203,7 +221,7 @@ class Engine
* Maps a callback to a framework method. * Maps a callback to a framework method.
* *
* @param string $name Method name * @param string $name Method name
* @param callback $callback Callback function * @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
*/ */
@ -218,11 +236,12 @@ class Engine
/** /**
* Registers a class to a framework method. * Registers a class to a framework method.
* @template T of object
* *
* @param string $name Method name * @param string $name Method name
* @param string $class Class name * @param class-string<T> $class Class name
* @param array $params Class initialization parameters * @param array<int, mixed> $params Class initialization parameters
* @param callable|null $callback $callback Function to call after object instantiation * @param ?callable(T $instance): void $callback Function to call after object instantiation
* *
* @throws Exception If trying to map over a framework method * @throws Exception If trying to map over a framework method
*/ */
@ -239,7 +258,7 @@ class Engine
* 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 callback $callback Callback function * @param callable $callback Callback function
*/ */
public function before(string $name, callable $callback): void public function before(string $name, callable $callback): void
{ {
@ -250,7 +269,7 @@ class Engine
* 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 callback $callback Callback function * @param callable $callback Callback function
*/ */
public function after(string $name, callable $callback): void public function after(string $name, callable $callback): void
{ {
@ -348,7 +367,7 @@ class Engine
// Flush any existing output // Flush any existing output
if (ob_get_length() > 0) { if (ob_get_length() > 0) {
$response->write(ob_get_clean()); $response->write(ob_get_clean()); // @codeCoverageIgnore
} }
// Enable output buffering // Enable output buffering
@ -392,7 +411,8 @@ class Engine
*/ */
public function _error($e): void public function _error($e): void
{ {
$msg = sprintf('<h1>500 Internal Server Error</h1>' . $msg = sprintf(
'<h1>500 Internal Server Error</h1>' .
'<h3>%s (%s)</h3>' . '<h3>%s (%s)</h3>' .
'<pre>%s</pre>', '<pre>%s</pre>',
$e->getMessage(), $e->getMessage(),
@ -406,9 +426,11 @@ class Engine
->status(500) ->status(500)
->write($msg) ->write($msg)
->send(); ->send();
// @codeCoverageIgnoreStart
} catch (Throwable $t) { } catch (Throwable $t) {
exit($msg); exit($msg);
} }
// @codeCoverageIgnoreEnd
} }
/** /**
@ -427,7 +449,8 @@ class Engine
$response->status($code); $response->status($code);
} }
$response->write(ob_get_clean()); $content = ob_get_clean();
$response->write($content ?: '');
$response->send(); $response->send();
} }
@ -437,7 +460,7 @@ class Engine
* Routes a URL to a callback function. * Routes a URL to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function _route(string $pattern, callable $callback, bool $pass_route = false): void public function _route(string $pattern, callable $callback, bool $pass_route = false): void
@ -449,7 +472,7 @@ class Engine
* Routes a URL to a callback function. * Routes a URL to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function _post(string $pattern, callable $callback, bool $pass_route = false): void public function _post(string $pattern, callable $callback, bool $pass_route = false): void
@ -461,7 +484,7 @@ class Engine
* Routes a URL to a callback function. * Routes a URL to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function _put(string $pattern, callable $callback, bool $pass_route = false): void public function _put(string $pattern, callable $callback, bool $pass_route = false): void
@ -473,7 +496,7 @@ class Engine
* Routes a URL to a callback function. * Routes a URL to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function _patch(string $pattern, callable $callback, bool $pass_route = false): void public function _patch(string $pattern, callable $callback, bool $pass_route = false): void
@ -485,7 +508,7 @@ class Engine
* Routes a URL to a callback function. * Routes a URL to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function _delete(string $pattern, callable $callback, bool $pass_route = false): void public function _delete(string $pattern, callable $callback, bool $pass_route = false): void
@ -498,6 +521,7 @@ class Engine
* *
* @param int $code HTTP status code * @param int $code HTTP status code
* @param string $message Response message * @param string $message Response message
*
*/ */
public function _halt(int $code = 200, string $message = ''): void public function _halt(int $code = 200, string $message = ''): void
{ {
@ -506,7 +530,10 @@ class Engine
->status($code) ->status($code)
->write($message) ->write($message)
->send(); ->send();
exit(); // apologies for the crappy hack here...
if($message !== 'skip---exit') {
exit(); // @codeCoverageIgnore
}
} }
/** /**
@ -555,7 +582,7 @@ class Engine
* Renders a template. * Renders a template.
* *
* @param string $file Template file * @param string $file Template file
* @param array|null $data Template data * @param ?array<string, mixed> $data Template data
* @param string|null $key View variable name * @param string|null $key View variable name
* *
* @throws Exception * @throws Exception
@ -639,8 +666,10 @@ class Engine
$this->response()->header('ETag', $id); $this->response()->header('ETag', $id);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && if (
$_SERVER['HTTP_IF_NONE_MATCH'] === $id) { isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
$_SERVER['HTTP_IF_NONE_MATCH'] === $id
) {
$this->halt(304); $this->halt(304);
} }
} }
@ -654,8 +683,10 @@ class Engine
{ {
$this->response()->header('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $time)); $this->response()->header('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $time));
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && if (
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) { isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time
) {
$this->halt(304); $this->halt(304);
} }
} }

@ -18,36 +18,27 @@ use flight\template\View;
/** /**
* The Flight class is a static representation of the framework. * The Flight class is a static representation of the framework.
* *
* Core.
*
* @method static void start() Starts the framework. * @method static void start() Starts the framework.
* @method static void path($path) Adds a path for autoloading classes. * @method static void path(string $path) Adds a path for autoloading classes.
* @method static void stop() Stops the framework and sends a response. * @method static void stop() Stops the framework and sends a response.
* @method static void halt($code = 200, $message = '') Stop the framework with an optional status code and message. * @method static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message.
* *
* Routing. * @method static void route(string $pattern, callable $callback, bool $pass_route = false) Maps a URL pattern to a callback.
* @method static void route($pattern, $callback) Maps a URL pattern to a callback.
* @method static Router router() Returns Router instance. * @method static Router router() Returns Router instance.
* *
* Extending & Overriding. * @method static void map(string $name, callable $callback) Creates a custom framework method.
* @method static void map($name, $callback) Creates a custom framework method.
* @method static void register($name, $class, array $params = array(), $callback = null) Registers a class to a framework method.
* *
* Filtering.
* @method static void before($name, $callback) Adds a filter before a framework method. * @method static void before($name, $callback) Adds a filter before a framework method.
* @method static void after($name, $callback) Adds a filter after a framework method. * @method static void after($name, $callback) Adds a filter after a framework method.
* *
* Variables.
* @method static void set($key, $value) Sets a variable. * @method static void set($key, $value) Sets a variable.
* @method static mixed get($key) Gets a variable. * @method static mixed get($key) Gets a variable.
* @method static bool has($key) Checks if a variable is set. * @method static bool has($key) Checks if a variable is set.
* @method static void clear($key = null) Clears a variable. * @method static void clear($key = null) Clears a variable.
* *
* Views.
* @method static void render($file, array $data = null, $key = null) Renders a template file. * @method static void render($file, array $data = null, $key = null) Renders a template file.
* @method static View view() Returns View instance. * @method static View view() Returns View instance.
* *
* Request & Response.
* @method static Request request() Returns Request instance. * @method static Request request() Returns Request instance.
* @method static Response response() Returns Response instance. * @method static Response response() Returns Response instance.
* @method static void redirect($url, $code = 303) Redirects to another URL. * @method static void redirect($url, $code = 303) Redirects to another URL.
@ -56,7 +47,6 @@ use flight\template\View;
* @method static void error($exception) Sends an HTTP 500 response. * @method static void error($exception) Sends an HTTP 500 response.
* @method static void notFound() Sends an HTTP 404 response. * @method static void notFound() Sends an HTTP 404 response.
* *
* HTTP Caching.
* @method static void etag($id, $type = 'strong') Performs ETag HTTP caching. * @method static void etag($id, $type = 'strong') Performs ETag HTTP caching.
* @method static void lastModified($time) Performs last modified HTTP caching. * @method static void lastModified($time) Performs last modified HTTP caching.
*/ */
@ -67,24 +57,50 @@ class Flight
*/ */
private static Engine $engine; private static Engine $engine;
// Don't allow object instantiation /**
* Don't allow object instantiation
*
* @codeCoverageIgnore
* @return void
*/
private function __construct() private function __construct()
{ {
} }
private function __destruct() /**
* Forbid cloning the class
*
* @codeCoverageIgnore
* @return void
*/
private function __clone()
{ {
} }
private function __clone() /**
* Registers a class to a framework method.
* @template T of object
* @param string $name Static method name
* ```
* Flight::register('user', User::class);
*
* Flight::user(); # <- Return a User instance
* ```
* @param class-string<T> $class Fully Qualified Class Name
* @param array<int, mixed> $params Class constructor params
* @param ?Closure(T $instance): void $callback Perform actions with the instance
* @return void
*/
static function register($name, $class, $params = array(), $callback = null)
{ {
static::__callStatic('register', func_get_args());
} }
/** /**
* Handles calls to static methods. * Handles calls to static methods.
* *
* @param string $name Method name * @param string $name Method name
* @param array $params Method parameters * @param array<int, mixed> $params Method parameters
* *
* @throws Exception * @throws Exception
* *

@ -23,11 +23,13 @@ class Dispatcher
{ {
/** /**
* Mapped events. * Mapped events.
* @var array<string, callable>
*/ */
protected array $events = []; protected array $events = [];
/** /**
* Method filters. * Method filters.
* @var array<string, array<'before'|'after', array<int, callable>>>
*/ */
protected array $filters = []; protected array $filters = [];
@ -35,7 +37,7 @@ class Dispatcher
* Dispatches an event. * Dispatches an event.
* *
* @param string $name Event name * @param string $name Event name
* @param array $params Callback parameters * @param array<int, mixed> $params Callback parameters
* *
* @throws Exception * @throws Exception
* *
@ -65,7 +67,7 @@ class Dispatcher
* Assigns a callback to an event. * Assigns a callback to an event.
* *
* @param string $name Event name * @param string $name Event name
* @param callback $callback Callback function * @param callable $callback Callback function
*/ */
final public function set(string $name, callable $callback): void final public function set(string $name, callable $callback): void
{ {
@ -77,7 +79,7 @@ class Dispatcher
* *
* @param string $name Event name * @param string $name Event name
* *
* @return callback $callback Callback function * @return callable $callback Callback function
*/ */
final public function get(string $name): ?callable final public function get(string $name): ?callable
{ {
@ -118,7 +120,7 @@ class Dispatcher
* *
* @param string $name Event name * @param string $name Event name
* @param string $type Filter type * @param string $type Filter type
* @param callback $callback Callback function * @param callable $callback Callback function
*/ */
final public function hook(string $name, string $type, callable $callback): void final public function hook(string $name, string $type, callable $callback): void
{ {
@ -128,8 +130,8 @@ class Dispatcher
/** /**
* Executes a chain of method filters. * Executes a chain of method filters.
* *
* @param array $filters Chain of filters * @param array<int, callable> $filters Chain of filters
* @param array $params Method parameters * @param array<int, mixed> $params Method parameters
* @param mixed $output Method output * @param mixed $output Method output
* *
* @throws Exception * @throws Exception
@ -148,8 +150,8 @@ class Dispatcher
/** /**
* Executes a callback function. * Executes a callback function.
* *
* @param array|callback $callback Callback function * @param callable|array<class-string|object, string> $callback Callback function
* @param array $params Function parameters * @param array<int, mixed> $params Function parameters
* *
* @throws Exception * @throws Exception
* *
@ -170,7 +172,7 @@ class Dispatcher
* Calls a function. * Calls a function.
* *
* @param callable|string $func Name of function to call * @param callable|string $func Name of function to call
* @param array $params Function parameters * @param array<int, mixed> $params Function parameters
* *
* @return mixed Function results * @return mixed Function results
*/ */
@ -203,7 +205,7 @@ class Dispatcher
* Invokes a method. * Invokes a method.
* *
* @param mixed $func Class method * @param mixed $func Class method
* @param array $params Class method parameters * @param array<int, mixed> $params Class method parameters
* *
* @return mixed Function results * @return mixed Function results
*/ */
@ -230,6 +232,8 @@ class Dispatcher
return ($instance) ? return ($instance) ?
$class->$method($params[0], $params[1], $params[2]) : $class->$method($params[0], $params[1], $params[2]) :
$class::$method($params[0], $params[1], $params[2]); $class::$method($params[0], $params[1], $params[2]);
// This will be refactored soon enough
// @codeCoverageIgnoreStart
case 4: case 4:
return ($instance) ? return ($instance) ?
$class->$method($params[0], $params[1], $params[2], $params[3]) : $class->$method($params[0], $params[1], $params[2], $params[3]) :
@ -240,6 +244,7 @@ class Dispatcher
$class::$method($params[0], $params[1], $params[2], $params[3], $params[4]); $class::$method($params[0], $params[1], $params[2], $params[3], $params[4]);
default: default:
return \call_user_func_array($func, $params); return \call_user_func_array($func, $params);
// @codeCoverageIgnoreEnd
} }
} }

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace flight\core; namespace flight\core;
use Closure;
use Exception; use Exception;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
@ -24,26 +25,30 @@ class Loader
{ {
/** /**
* Registered classes. * Registered classes.
* @var array<string, array{class-string, array<int, mixed>, ?callable}> $classes
*/ */
protected array $classes = []; protected array $classes = [];
/** /**
* Class instances. * Class instances.
* @var array<string, object>
*/ */
protected array $instances = []; protected array $instances = [];
/** /**
* Autoload directories. * Autoload directories.
* @var array<int, string>
*/ */
protected static array $dirs = []; protected static array $dirs = [];
/** /**
* Registers a class. * Registers a class.
* @template T of object
* *
* @param string $name Registry name * @param string $name Registry name
* @param callable|string $class Class name or function to instantiate class * @param class-string<T> $class Class name or function to instantiate class
* @param array $params Class initialization parameters * @param array<int, mixed> $params Class initialization parameters
* @param callable|null $callback $callback Function to call after object instantiation * @param ?callable(T $instance): void $callback $callback Function to call after object instantiation
*/ */
public function register(string $name, $class, array $params = [], ?callable $callback = null): void public function register(string $name, $class, array $params = [], ?callable $callback = null): void
{ {
@ -77,7 +82,7 @@ class Loader
$obj = null; $obj = null;
if (isset($this->classes[$name])) { if (isset($this->classes[$name])) {
[$class, $params, $callback] = $this->classes[$name]; [0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name];
$exists = isset($this->instances[$name]); $exists = isset($this->instances[$name]);
@ -116,15 +121,16 @@ class Loader
/** /**
* Gets a new instance of a class. * Gets a new instance of a class.
* @template T of object
* *
* @param callable|string $class Class name or callback function to instantiate class * @param class-string<T>|Closure(): class-string<T> $class Class name or callback function to instantiate class
* @param array $params Class initialization parameters * @param array<int, string> $params Class initialization parameters
* *
* @throws Exception * @throws Exception
* *
* @return object Class instance * @return T Class instance
*/ */
public function newInstance($class, array $params = []): object public function newInstance($class, array $params = [])
{ {
if (\is_callable($class)) { if (\is_callable($class)) {
return \call_user_func_array($class, $params); return \call_user_func_array($class, $params);
@ -135,6 +141,7 @@ class Loader
return new $class(); return new $class();
case 1: case 1:
return new $class($params[0]); return new $class($params[0]);
// @codeCoverageIgnoreStart
case 2: case 2:
return new $class($params[0], $params[1]); return new $class($params[0], $params[1]);
case 3: case 3:
@ -143,6 +150,7 @@ class Loader
return new $class($params[0], $params[1], $params[2], $params[3]); return new $class($params[0], $params[1], $params[2], $params[3]);
case 5: case 5:
return new $class($params[0], $params[1], $params[2], $params[3], $params[4]); return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
// @codeCoverageIgnoreEnd
default: default:
try { try {
$refClass = new ReflectionClass($class); $refClass = new ReflectionClass($class);
@ -179,14 +187,14 @@ class Loader
* Starts/stops autoloader. * Starts/stops autoloader.
* *
* @param bool $enabled Enable/disable autoloading * @param bool $enabled Enable/disable autoloading
* @param mixed $dirs Autoload directories * @param string|iterable<int, string> $dirs Autoload directories
*/ */
public static function autoload(bool $enabled = true, $dirs = []): void public static function autoload(bool $enabled = true, $dirs = []): void
{ {
if ($enabled) { if ($enabled) {
spl_autoload_register([__CLASS__, 'loadClass']); spl_autoload_register([__CLASS__, 'loadClass']);
} else { } else {
spl_autoload_unregister([__CLASS__, 'loadClass']); spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore
} }
if (!empty($dirs)) { if (!empty($dirs)) {
@ -216,7 +224,7 @@ class Loader
/** /**
* Adds a directory for autoloading classes. * Adds a directory for autoloading classes.
* *
* @param mixed $dir Directory path * @param string|iterable<int, string> $dir Directory path
*/ */
public static function addDirectory($dir): void public static function addDirectory($dir): void
{ {

@ -18,23 +18,24 @@ use flight\util\Collection;
* are stored and accessible via the Request object. * are stored and accessible via the Request object.
* *
* The default request properties are: * The default request properties are:
* url - The URL being requested *
* base - The parent subdirectory of the URL * - **url** - The URL being requested
* method - The request method (GET, POST, PUT, DELETE) * - **base** - The parent subdirectory of the URL
* referrer - The referrer URL * - **method** - The request method (GET, POST, PUT, DELETE)
* ip - IP address of the client * - **referrer** - The referrer URL
* ajax - Whether the request is an AJAX request * - **ip** - IP address of the client
* scheme - The server protocol (http, https) * - **ajax** - Whether the request is an AJAX request
* user_agent - Browser information * - **scheme** - The server protocol (http, https)
* type - The content type * - **user_agent** - Browser information
* length - The content length * - **type** - The content type
* query - Query string parameters * - **length** - The content length
* data - Post parameters * - **query** - Query string parameters
* cookies - Cookie parameters * - **data** - Post parameters
* files - Uploaded files * - **cookies** - Cookie parameters
* secure - Connection is secure * - **files** - Uploaded files
* accept - HTTP accept parameters * - **secure** - Connection is secure
* proxy_ip - Proxy IP address of the client * - **accept** - HTTP accept parameters
* - **proxy_ip** - Proxy IP address of the client
*/ */
final class Request final class Request
{ {
@ -128,12 +129,25 @@ final class Request
*/ */
public string $host; public string $host;
/**
* Stream path for where to pull the request body from
*
* @var string
*/
private string $stream_path = 'php://input';
/**
* @var string Raw HTTP request body
*/
public string $body = '';
/** /**
* Constructor. * Constructor.
* *
* @param array $config Request configuration * @param array<string, mixed> $config Request configuration
* @param string
*/ */
public function __construct(array $config = []) public function __construct($config = array())
{ {
// Default properties // Default properties
if (empty($config)) { if (empty($config)) {
@ -165,7 +179,8 @@ final class Request
/** /**
* Initialize request properties. * Initialize request properties.
* *
* @param array $properties Array of request properties * @param array<string, mixed> $properties Array of request properties
* @return static
*/ */
public function init(array $properties = []) public function init(array $properties = [])
{ {
@ -175,6 +190,9 @@ final class Request
} }
// Get the requested URL without the base directory // Get the requested URL without the base directory
// This rewrites the url in case the public url and base directories match
// (such as installing on a subdirectory in a web server)
// @see testInitUrlSameAsBaseDirectory
if ('/' !== $this->base && '' !== $this->base && 0 === strpos($this->url, $this->base)) { if ('/' !== $this->base && '' !== $this->base && 0 === strpos($this->url, $this->base)) {
$this->url = substr($this->url, \strlen($this->base)); $this->url = substr($this->url, \strlen($this->base));
} }
@ -191,7 +209,7 @@ final class Request
// Check for JSON input // Check for JSON input
if (0 === strpos($this->type, 'application/json')) { if (0 === strpos($this->type, 'application/json')) {
$body = self::getBody(); $body = $this->getBody();
if ('' !== $body && null !== $body) { if ('' !== $body && null !== $body) {
$data = json_decode($body, true); $data = json_decode($body, true);
if (is_array($data)) { if (is_array($data)) {
@ -199,6 +217,8 @@ final class Request
} }
} }
} }
return $this;
} }
/** /**
@ -206,20 +226,22 @@ final class Request
* *
* @return string Raw HTTP request body * @return string Raw HTTP request body
*/ */
public static function getBody(): ?string public function getBody(): ?string
{ {
static $body; $body = $this->body;
if (null !== $body) { if ('' !== $body) {
return $body; return $body;
} }
$method = self::getMethod(); $method = self::getMethod();
if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) { if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) {
$body = file_get_contents('php://input'); $body = file_get_contents($this->stream_path);
} }
$this->body = $body;
return $body; return $body;
} }
@ -287,11 +309,11 @@ final class Request
* *
* @param string $url URL string * @param string $url URL string
* *
* @return array Query parameters * @return array<string, int|string|array<int|string, int|string>>
*/ */
public static function parseQuery(string $url): array public static function parseQuery(string $url): array
{ {
$params = []; $params = array();
$args = parse_url($url); $args = parse_url($url);
if (isset($args['query'])) { if (isset($args['query'])) {

@ -25,7 +25,7 @@ class Response
public bool $content_length = true; public bool $content_length = true;
/** /**
* @var array HTTP status codes * @var array<int, ?string> HTTP status codes
*/ */
public static array $codes = [ public static array $codes = [
100 => 'Continue', 100 => 'Continue',
@ -103,7 +103,7 @@ class Response
protected int $status = 200; protected int $status = 200;
/** /**
* @var array HTTP headers * @var array<string, int|string|array<int, string>> HTTP headers
*/ */
protected array $headers = []; protected array $headers = [];
@ -124,7 +124,7 @@ class Response
* *
* @throws Exception If invalid status code * @throws Exception If invalid status code
* *
* @return int|object Self reference * @return int|static Self reference
*/ */
public function status(?int $code = null) public function status(?int $code = null)
{ {
@ -144,10 +144,10 @@ class Response
/** /**
* Adds a header to the response. * Adds a header to the response.
* *
* @param array|string $name Header name or array of names and values * @param array<string, int|string>|string $name Header name or array of names and values
* @param string|null $value Header value * @param string|null $value Header value
* *
* @return object Self reference * @return static Self reference
*/ */
public function header($name, ?string $value = null) public function header($name, ?string $value = null)
{ {
@ -164,8 +164,7 @@ class Response
/** /**
* Returns the headers from the response. * Returns the headers from the response.
* * @return array<string, int|string|array<int, string>>
* @return array
*/ */
public function headers() public function headers()
{ {
@ -203,7 +202,7 @@ class Response
/** /**
* Sets caching headers for the response. * Sets caching headers for the response.
* *
* @param int|string $expires Expiration time * @param int|string|false $expires Expiration time as time() or as strtotime() string value
* *
* @return Response Self reference * @return Response Self reference
*/ */
@ -238,7 +237,8 @@ class Response
{ {
// Send status code header // Send status code header
if (false !== strpos(\PHP_SAPI, 'cgi')) { if (false !== strpos(\PHP_SAPI, 'cgi')) {
header( // @codeCoverageIgnoreStart
$this->setRealHeader(
sprintf( sprintf(
'Status: %d %s', 'Status: %d %s',
$this->status, $this->status,
@ -246,13 +246,15 @@ class Response
), ),
true true
); );
// @codeCoverageIgnoreEnd
} else { } else {
header( $this->setRealHeader(
sprintf( sprintf(
'%s %d %s', '%s %d %s',
$_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1', $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
$this->status, $this->status,
self::$codes[$this->status]), self::$codes[$this->status]
),
true, true,
$this->status $this->status
); );
@ -262,10 +264,10 @@ class Response
foreach ($this->headers as $field => $value) { foreach ($this->headers as $field => $value) {
if (\is_array($value)) { if (\is_array($value)) {
foreach ($value as $v) { foreach ($value as $v) {
header($field . ': ' . $v, false); $this->setRealHeader($field . ': ' . $v, false);
} }
} else { } else {
header($field . ': ' . $value); $this->setRealHeader($field . ': ' . $value);
} }
} }
@ -274,13 +276,27 @@ class Response
$length = $this->getContentLength(); $length = $this->getContentLength();
if ($length > 0) { if ($length > 0) {
header('Content-Length: ' . $length); $this->setRealHeader('Content-Length: ' . $length);
} }
} }
return $this; return $this;
} }
/**
* Sets a real header. Mostly used for test mocking.
*
* @param string $header_string The header string you would pass to header()
* @param bool $replace The optional replace parameter indicates whether the header should replace a previous similar header, or add a second header of the same type. By default it will replace, but if you pass in false as the second argument you can force multiple headers of the same type.
* @param int $response_code The response code to send
* @return self
* @codeCoverageIgnore
*/
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self {
header($header_string, $replace, $response_code);
return $this;
}
/** /**
* Gets the content length. * Gets the content length.
* *
@ -294,7 +310,7 @@ class Response
} }
/** /**
* Gets whether response was sent. * Gets whether response body was sent.
*/ */
public function sent(): bool public function sent(): bool
{ {
@ -307,11 +323,11 @@ class Response
public function send(): void public function send(): void
{ {
if (ob_get_length() > 0) { if (ob_get_length() > 0) {
ob_end_clean(); ob_end_clean(); // @codeCoverageIgnore
} }
if (!headers_sent()) { if (!headers_sent()) {
$this->sendHeaders(); $this->sendHeaders(); // @codeCoverageIgnore
} }
echo $this->body; echo $this->body;

@ -28,12 +28,12 @@ final class Route
public $callback; public $callback;
/** /**
* @var array HTTP methods * @var array<int, string> HTTP methods
*/ */
public array $methods = []; public array $methods = [];
/** /**
* @var array Route parameters * @var array<int, ?string> Route parameters
*/ */
public array $params = []; public array $params = [];
@ -56,8 +56,8 @@ final class Route
* Constructor. * Constructor.
* *
* @param string $pattern URL pattern * @param string $pattern URL pattern
* @param mixed $callback Callback function * @param callable $callback Callback function
* @param array $methods HTTP methods * @param array<int, string> $methods HTTP methods
* @param bool $pass Pass self in callback parameters * @param bool $pass Pass self in callback parameters
*/ */
public function __construct(string $pattern, $callback, array $methods, bool $pass) public function __construct(string $pattern, $callback, array $methods, bool $pass)

@ -23,6 +23,7 @@ class Router
public bool $case_sensitive = false; public bool $case_sensitive = false;
/** /**
* Mapped routes. * Mapped routes.
* @var array<int, Route>
*/ */
protected array $routes = []; protected array $routes = [];
@ -34,7 +35,7 @@ class Router
/** /**
* Gets mapped routes. * Gets mapped routes.
* *
* @return array Array of routes * @return array<int, Route> Array of routes
*/ */
public function getRoutes(): array public function getRoutes(): array
{ {
@ -53,7 +54,7 @@ class Router
* Maps a URL pattern to a callback function. * Maps a URL pattern to a callback function.
* *
* @param string $pattern URL pattern to match * @param string $pattern URL pattern to match
* @param callback $callback Callback function * @param callable $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback * @param bool $pass_route Pass the matching route object to the callback
*/ */
public function map(string $pattern, callable $callback, bool $pass_route = false): void public function map(string $pattern, callable $callback, bool $pass_route = false): void
@ -81,7 +82,7 @@ class Router
{ {
$url_decoded = urldecode($request->url); $url_decoded = urldecode($request->url);
while ($route = $this->current()) { while ($route = $this->current()) {
if (false !== $route && $route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) { if ($route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) {
return $route; return $route;
} }
$this->next(); $this->next();

@ -34,7 +34,7 @@ class View
/** /**
* View variables. * View variables.
* *
* @var array * @var array<string, mixed>
*/ */
protected $vars = []; protected $vars = [];
@ -70,8 +70,9 @@ class View
/** /**
* Sets a template variable. * Sets a template variable.
* *
* @param mixed $key Key * @param string|iterable<string, mixed> $key Key
* @param string $value Value * @param mixed $value Value
* @return static
*/ */
public function set($key, $value = null) public function set($key, $value = null)
{ {
@ -82,6 +83,8 @@ class View
} else { } else {
$this->vars[$key] = $value; $this->vars[$key] = $value;
} }
return $this;
} }
/** /**
@ -100,6 +103,7 @@ class View
* Unsets a template variable. If no key is passed in, clear all variables. * Unsets a template variable. If no key is passed in, clear all variables.
* *
* @param string $key Key * @param string $key Key
* @return static
*/ */
public function clear($key = null) public function clear($key = null)
{ {
@ -108,15 +112,18 @@ class View
} else { } else {
unset($this->vars[$key]); unset($this->vars[$key]);
} }
return $this;
} }
/** /**
* Renders a template. * Renders a template.
* *
* @param string $file Template file * @param string $file Template file
* @param array $data Template data * @param array<string, mixed> $data Template data
* *
* @throws \Exception If template not found * @throws \Exception If template not found
* @return void
*/ */
public function render($file, $data = null) public function render($file, $data = null)
{ {
@ -139,7 +146,7 @@ class View
* Gets the output of a template. * Gets the output of a template.
* *
* @param string $file Template file * @param string $file Template file
* @param array $data Template data * @param array<string, mixed> $data Template data
* *
* @return string Output of template * @return string Output of template
*/ */
@ -179,11 +186,13 @@ class View
$file .= $ext; $file .= $ext;
} }
if (('/' == substr($file, 0, 1))) { $is_windows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
if (('/' == substr($file, 0, 1)) || ($is_windows === true && ':' == substr($file, 1, 1))) {
return $file; return $file;
} }
return $this->path . '/' . $file; return $this->path . DIRECTORY_SEPARATOR . $file;
} }
/** /**
@ -195,6 +204,8 @@ class View
*/ */
public function e($str) public function e($str)
{ {
echo htmlentities($str); $value = htmlentities($str);
echo $value;
return $value;
} }
} }

@ -11,30 +11,32 @@ declare(strict_types=1);
namespace flight\util; namespace flight\util;
use ArrayAccess; use ArrayAccess;
use function count;
use Countable; use Countable;
use Iterator; use Iterator;
use JsonSerializable; use JsonSerializable;
if (!interface_exists('JsonSerializable')) { if (!interface_exists('JsonSerializable')) {
require_once __DIR__ . '/LegacyJsonSerializable.php'; require_once __DIR__ . '/LegacyJsonSerializable.php'; // @codeCoverageIgnore
} }
/** /**
* The Collection class allows you to access a set of data * The Collection class allows you to access a set of data
* using both array and object notation. * using both array and object notation.
* @implements ArrayAccess<string, mixed>
* @implements Iterator<string, mixed>
*/ */
final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
{ {
/** /**
* Collection data. * Collection data.
* @var array<string, mixed>
*/ */
private array $data; private array $data;
/** /**
* Constructor. * Constructor.
* *
* @param array $data Initial data * @param array<string, mixed> $data Initial data
*/ */
public function __construct(array $data = []) public function __construct(array $data = [])
{ {
@ -102,11 +104,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
/** /**
* Sets an item at the offset. * Sets an item at the offset.
* *
* @param string $offset Offset * @param ?string $offset Offset
* @param mixed $value Value * @param mixed $value Value
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetSet($offset, $value) public function offsetSet($offset, $value): void
{ {
if (null === $offset) { if (null === $offset) {
$this->data[] = $value; $this->data[] = $value;
@ -169,13 +171,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
/** /**
* Gets the next collection value. * Gets the next collection value.
*
* @return mixed Value
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function next() public function next(): void
{ {
return next($this->data); next($this->data);
} }
/** /**
@ -187,7 +187,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
{ {
$key = key($this->data); $key = key($this->data);
return null !== $key && false !== $key; return null !== $key;
} }
/** /**
@ -203,7 +203,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
/** /**
* Gets the item keys. * Gets the item keys.
* *
* @return array Collection keys * @return array<int, string> Collection keys
*/ */
public function keys(): array public function keys(): array
{ {
@ -213,7 +213,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
/** /**
* Gets the collection data. * Gets the collection data.
* *
* @return array Collection data * @return array<string, mixed> Collection data
*/ */
public function getData(): array public function getData(): array
{ {
@ -223,19 +223,15 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
/** /**
* Sets the collection data. * Sets the collection data.
* *
* @param array $data New collection data * @param array<string, mixed> $data New collection data
*/ */
public function setData(array $data): void public function setData(array $data): void
{ {
$this->data = $data; $this->data = $data;
} }
/** #[\ReturnTypeWillChange]
* Gets the collection data which can be serialized to JSON. public function jsonSerialize()
*
* @return array Collection data which can be serialized by <b>json_encode</b>
*/
public function jsonSerialize(): array
{ {
return $this->data; return $this->data;
} }

@ -9,5 +9,9 @@ declare(strict_types=1);
*/ */
interface LegacyJsonSerializable interface LegacyJsonSerializable
{ {
/**
* Gets the collection data which can be serialized to JSON.
* @return mixed Collection data which can be serialized by <b>json_encode</b>
*/
public function jsonSerialize(); public function jsonSerialize();
} }

@ -0,0 +1,3 @@
<?php
class ReturnTypeWillChange {}

@ -0,0 +1,10 @@
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
parameters:
level: 6
excludePaths:
- vendor
paths:
- flight
- index.php

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="tests/phpunit_autoload.php"
executionOrder="random"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
stopOnError="true"
stopOnFailure="true"
verbose="true"
colors="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">flight/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="default">
<directory>tests/</directory>
</testsuite>
</testsuites>
<logging/>
</phpunit>

@ -8,9 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class AutoloadTest extends PHPUnit\Framework\TestCase class AutoloadTest extends PHPUnit\Framework\TestCase
{ {
/** /**

@ -0,0 +1,98 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
class CollectionTest extends PHPUnit\Framework\TestCase
{
/**
* @var \flight\util\Collection
*/
private $collection;
protected function setUp(): void
{
$this->collection = new \flight\util\Collection(['a' => 1, 'b' => 2]);
}
// Get an item
public function testGet()
{
$this->assertEquals(1, $this->collection->a);
}
// Set an item
public function testSet()
{
$this->collection->c = 3;
$this->assertEquals(3, $this->collection->c);
}
// Check if an item exists
public function testExists()
{
$this->assertTrue(isset($this->collection->a));
}
// Unset an item
public function testUnset()
{
unset($this->collection->a);
$this->assertFalse(isset($this->collection->a));
}
// Count items
public function testCount()
{
$this->assertEquals(2, count($this->collection));
}
// Iterate through items
public function testIterate()
{
$items = [];
foreach ($this->collection as $key => $value) {
$items[$key] = $value;
}
$this->assertEquals(['a' => 1, 'b' => 2], $items);
}
public function testJsonSerialize()
{
$this->assertEquals(['a' => 1, 'b' => 2], $this->collection->jsonSerialize());
}
public function testOffsetSetWithNullOffset() {
$this->collection->offsetSet(null, 3);
$this->assertEquals(3, $this->collection->offsetGet(0));
}
public function testOffsetExists() {
$this->collection->a = 1;
$this->assertTrue($this->collection->offsetExists('a'));
}
public function testOffsetUnset() {
$this->collection->a = 1;
$this->assertTrue($this->collection->offsetExists('a'));
$this->collection->offsetUnset('a');
$this->assertFalse($this->collection->offsetExists('a'));
}
public function testKeys() {
$this->collection->a = 1;
$this->collection->b = 2;
$this->assertEquals(['a', 'b'], $this->collection->keys());
}
public function testClear() {
$this->collection->a = 1;
$this->collection->b = 2;
$this->assertEquals(['a', 'b'], $this->collection->keys());
$this->collection->clear();
$this->assertEquals(0, $this->collection->count());
}
}

@ -8,9 +8,6 @@
use flight\core\Dispatcher; use flight\core\Dispatcher;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/classes/Hello.php';
class DispatcherTest extends PHPUnit\Framework\TestCase class DispatcherTest extends PHPUnit\Framework\TestCase
{ {
/** /**
@ -47,6 +44,61 @@ class DispatcherTest extends PHPUnit\Framework\TestCase
self::assertEquals('hello', $result); self::assertEquals('hello', $result);
} }
public function testHasEvent()
{
$this->dispatcher->set('map-event', function () {
return 'hello';
});
$result = $this->dispatcher->has('map-event');
$this->assertTrue($result);
}
public function testClearAllRegisteredEvents() {
$this->dispatcher->set('map-event', function () {
return 'hello';
});
$this->dispatcher->set('map-event-2', function () {
return 'there';
});
$this->dispatcher->clear();
$result = $this->dispatcher->has('map-event');
$this->assertFalse($result);
$result = $this->dispatcher->has('map-event-2');
$this->assertFalse($result);
}
public function testClearDeclaredRegisteredEvent() {
$this->dispatcher->set('map-event', function () {
return 'hello';
});
$this->dispatcher->set('map-event-2', function () {
return 'there';
});
$this->dispatcher->clear('map-event');
$result = $this->dispatcher->has('map-event');
$this->assertFalse($result);
$result = $this->dispatcher->has('map-event-2');
$this->assertTrue($result);
}
// Map a static function
public function testStaticFunctionMapping()
{
$this->dispatcher->set('map2', 'Hello::sayHi');
$result = $this->dispatcher->run('map2');
self::assertEquals('hello', $result);
}
// Map a class method // Map a class method
public function testClassMethodMapping() public function testClassMethodMapping()
{ {
@ -98,4 +150,31 @@ class DispatcherTest extends PHPUnit\Framework\TestCase
$this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']);
} }
public function testCallFunction4Params() {
$closure = function($param1, $params2, $params3, $param4) {
return 'hello'.$param1.$params2.$params3.$param4;
};
$params = ['param1', 'param2', 'param3', 'param4'];
$result = $this->dispatcher->callFunction($closure, $params);
$this->assertEquals('helloparam1param2param3param4', $result);
}
public function testCallFunction5Params() {
$closure = function($param1, $params2, $params3, $param4, $param5) {
return 'hello'.$param1.$params2.$params3.$param4.$param5;
};
$params = ['param1', 'param2', 'param3', 'param4', 'param5'];
$result = $this->dispatcher->callFunction($closure, $params);
$this->assertEquals('helloparam1param2param3param4param5', $result);
}
public function testCallFunction6Params() {
$closure = function($param1, $params2, $params3, $param4, $param5, $param6) {
return 'hello'.$param1.$params2.$params3.$param4.$param5.$param6;
};
$params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6'];
$result = $this->dispatcher->callFunction($closure, $params);
$this->assertEquals('helloparam1param2param3param4param5param6', $result);
}
} }

@ -0,0 +1,268 @@
<?php
use flight\Engine;
use flight\net\Response;
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
class EngineTest extends PHPUnit\Framework\TestCase
{
public function setUp(): void {
$_SERVER = [];
}
public function tearDown(): void {
$_SERVER = [];
}
public function testInitBeforeStart() {
$engine = new class extends Engine {
public function getInitializedVar() {
return $this->initialized;
}
};
$this->assertTrue($engine->getInitializedVar());
$engine->start();
// this is necessary cause it doesn't actually send the response correctly
ob_end_clean();
$this->assertFalse($engine->router()->case_sensitive);
$this->assertTrue($engine->response()->content_length);
}
public function testHandleErrorNoErrorNumber() {
$engine = new Engine();
$result = $engine->handleError(0, '', '', 0);
$this->assertFalse($result);
}
public function testHandleErrorWithException() {
$engine = new Engine();
$this->expectException(Exception::class);
$this->expectExceptionCode(5);
$this->expectExceptionMessage('thrown error message');
$engine->handleError(5, 'thrown error message', '', 0);
}
public function testHandleException() {
$engine = new Engine();
$regex_message = preg_quote('<h1>500 Internal Server Error</h1><h3>thrown exception message (20)</h3>');
$this->expectOutputRegex('~'.$regex_message.'~');
$engine->handleException(new Exception('thrown exception message', 20));
}
public function testMapExistingMethod() {
$engine = new Engine();
$this->expectException(Exception::class);
$this->expectExceptionMessage('Cannot override an existing framework method.');
$engine->map('_start', function() {});
}
public function testRegisterExistingMethod() {
$engine = new Engine();
$this->expectException(Exception::class);
$this->expectExceptionMessage('Cannot override an existing framework method.');
$engine->register('_error', 'stdClass');
}
public function testSetArrayOfValues() {
$engine = new Engine();
$engine->set([ 'key1' => 'value1', 'key2' => 'value2']);
$this->assertEquals('value1', $engine->get('key1'));
$this->assertEquals('value2', $engine->get('key2'));
}
public function testStartWithRoute() {
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/someRoute';
$engine = new class extends Engine {
public function getInitializedVar() {
return $this->initialized;
}
};
$engine->route('/someRoute', function() { echo 'i ran'; }, true);
$this->expectOutputString('i ran');
$engine->start();
}
// n0nag0n - I don't know why this does what it does, but it's existing framework functionality 1/1/24
public function testStartWithRouteButReturnedValueThrows404() {
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/someRoute';
$engine = new class extends Engine {
public function getInitializedVar() {
return $this->initialized;
}
};
$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 testStopWithCode() {
$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 \flight\net\Response {
public function __construct() {}
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
{
return $this;
}
};
});
// need to add another one of these because _stop() stops and gets clean, but $response->send() does too.....
ob_start();
$engine->response()->write('I am a teapot');
$this->expectOutputString('I am a teapot');
$engine->stop(500);
$this->assertEquals(500, $engine->response()->status());
}
public function testPostRoute() {
$engine = new Engine();
$engine->post('/someRoute', function() { echo 'i ran'; }, true);
$routes = $engine->router()->getRoutes();
$this->assertEquals('POST', $routes[0]->methods[0]);
$this->assertEquals('/someRoute', $routes[0]->pattern);
}
public function testPutRoute() {
$engine = new Engine();
$engine->put('/someRoute', function() { echo 'i ran'; }, true);
$routes = $engine->router()->getRoutes();
$this->assertEquals('PUT', $routes[0]->methods[0]);
$this->assertEquals('/someRoute', $routes[0]->pattern);
}
public function testPatchRoute() {
$engine = new Engine();
$engine->patch('/someRoute', function() { echo 'i ran'; }, true);
$routes = $engine->router()->getRoutes();
$this->assertEquals('PATCH', $routes[0]->methods[0]);
$this->assertEquals('/someRoute', $routes[0]->pattern);
}
public function testDeleteRoute() {
$engine = new Engine();
$engine->delete('/someRoute', function() { echo 'i ran'; }, true);
$routes = $engine->router()->getRoutes();
$this->assertEquals('DELETE', $routes[0]->methods[0]);
$this->assertEquals('/someRoute', $routes[0]->pattern);
}
public function testHalt() {
$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 \flight\net\Response {
public function __construct() {}
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
{
return $this;
}
};
});
$this->expectOutputString('skip---exit');
$engine->halt(500, 'skip---exit');
$this->assertEquals(500, $engine->response()->status());
}
public function testRedirect() {
$engine = new Engine();
$engine->redirect('https://github.com', 302);
$this->assertEquals('https://github.com', $engine->response()->headers()['Location']);
$this->assertEquals(302, $engine->response()->status());
}
public function testRedirectWithBaseUrl() {
$engine = new Engine();
$engine->set('flight.base_url', '/subdirectory');
$engine->redirect('/someRoute', 301);
$this->assertEquals('/subdirectory/someRoute', $engine->response()->headers()['Location']);
$this->assertEquals(301, $engine->response()->status());
}
public function testJson() {
$engine = new Engine();
$engine->json(['key1' => 'value1', 'key2' => 'value2']);
$this->expectOutputString('{"key1":"value1","key2":"value2"}');
$this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']);
$this->assertEquals(200, $engine->response()->status());
}
public function testJsonP() {
$engine = new Engine();
$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() {
$engine = new Engine();
$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() {
$engine = new Engine();
$engine->etag('etag');
$this->assertEquals('etag', $engine->response()->headers()['ETag']);
}
public function testEtagWithHttpIfNoneMatch() {
// just need this not to exit...
$engine = new class extends Engine {
public function _halt(int $code = 200, string $message = ''): void
{
$this->response()->status($code);
$this->response()->write($message);
}
};
$_SERVER['HTTP_IF_NONE_MATCH'] = 'etag';
$engine->etag('etag');
$this->assertEquals('etag', $engine->response()->headers()['ETag']);
$this->assertEquals(304, $engine->response()->status());
}
public function testLastModifiedSimple() {
$engine = new Engine();
$engine->lastModified(1234567890);
$this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']);
}
public function testLastModifiedWithHttpIfModifiedSince() {
// just need this not to exit...
$engine = new class extends Engine {
public function _halt(int $code = 200, string $message = ''): void
{
$this->response()->status($code);
$this->response()->write($message);
}
};
$_SERVER['HTTP_IF_MODIFIED_SINCE'] = 'Fri, 13 Feb 2009 23:31:30 GMT';
$engine->lastModified(1234567890);
$this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']);
$this->assertEquals(304, $engine->response()->status());
}
}

@ -8,9 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class FilterTest extends PHPUnit\Framework\TestCase class FilterTest extends PHPUnit\Framework\TestCase
{ {
/** /**

@ -11,16 +11,20 @@ use flight\template\View;
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com> * @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license * @license MIT, http://flightphp.com/license
*/ */
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/Flight.php';
class FlightTest extends PHPUnit\Framework\TestCase class FlightTest extends PHPUnit\Framework\TestCase
{ {
protected function setUp(): void protected function setUp(): void
{ {
$_SERVER = [];
$_REQUEST = [];
Flight::init(); Flight::init();
} }
protected function tearDown(): void {
unset($_REQUEST);
unset($_SERVER);
}
// Checks that default components are loaded // Checks that default components are loaded
public function testDefaultComponents() public function testDefaultComponents()
{ {

@ -8,9 +8,9 @@
use flight\core\Loader; use flight\core\Loader;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/classes/User.php'; require_once __DIR__ . '/classes/User.php';
require_once __DIR__ . '/classes/Factory.php'; require_once __DIR__ . '/classes/Factory.php';
require_once __DIR__ . '/classes/TesterClass.php';
class LoaderTest extends PHPUnit\Framework\TestCase class LoaderTest extends PHPUnit\Framework\TestCase
{ {
@ -118,4 +118,42 @@ class LoaderTest extends PHPUnit\Framework\TestCase
self::assertIsObject($obj); self::assertIsObject($obj);
self::assertInstanceOf(Factory::class, $obj); self::assertInstanceOf(Factory::class, $obj);
} }
public function testUnregisterClass() {
$this->loader->register('g', 'User');
$current_class = $this->loader->get('g');
$this->assertEquals([ 'User', [], null ], $current_class);
$this->loader->unregister('g');
$unregistered_class_result = $this->loader->get('g');
$this->assertNull($unregistered_class_result);
}
public function testNewInstance6Params() {
$TesterClass = $this->loader->newInstance('TesterClass', ['Bob','Fred', 'Joe', 'Jane', 'Sally', 'Suzie']);
$this->assertEquals('Bob', $TesterClass->param1);
$this->assertEquals('Fred', $TesterClass->param2);
$this->assertEquals('Joe', $TesterClass->param3);
$this->assertEquals('Jane', $TesterClass->param4);
$this->assertEquals('Sally', $TesterClass->param5);
$this->assertEquals('Suzie', $TesterClass->param6);
}
public function testNewInstance6ParamsBadClass() {
$this->expectException(Exception::class);
$this->expectExceptionMessage('Cannot instantiate BadClass');
$TesterClass = $this->loader->newInstance('BadClass', ['Bob','Fred', 'Joe', 'Jane', 'Sally', 'Suzie']);
}
public function testAddDirectoryAsArray() {
$loader = new class extends Loader {
public function getDirectories() {
return self::$dirs;
}
};
$loader->addDirectory([__DIR__ . '/classes']);
self::assertEquals([
dirname(__DIR__),
__DIR__ . '/classes'
], $loader->getDirectories());
}
} }

@ -8,8 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
require_once __DIR__ . '/classes/Hello.php'; require_once __DIR__ . '/classes/Hello.php';
class MapTest extends PHPUnit\Framework\TestCase class MapTest extends PHPUnit\Framework\TestCase

@ -8,9 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class RedirectTest extends PHPUnit\Framework\TestCase class RedirectTest extends PHPUnit\Framework\TestCase
{ {
private Engine $app; private Engine $app;

@ -8,8 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
require_once __DIR__ . '/classes/User.php'; require_once __DIR__ . '/classes/User.php';
class RegisterTest extends PHPUnit\Framework\TestCase class RegisterTest extends PHPUnit\Framework\TestCase

@ -8,9 +8,6 @@
use flight\Engine; use flight\Engine;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/Flight.php';
class RenderTest extends PHPUnit\Framework\TestCase class RenderTest extends PHPUnit\Framework\TestCase
{ {
private Engine $app; private Engine $app;

@ -7,9 +7,7 @@
*/ */
use flight\net\Request; use flight\net\Request;
use flight\util\Collection;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class RequestTest extends PHPUnit\Framework\TestCase class RequestTest extends PHPUnit\Framework\TestCase
{ {
@ -17,6 +15,8 @@ class RequestTest extends PHPUnit\Framework\TestCase
protected function setUp(): void protected function setUp(): void
{ {
$_SERVER = [];
$_REQUEST = [];
$_SERVER['REQUEST_URI'] = '/'; $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_METHOD'] = 'GET';
@ -34,6 +34,11 @@ class RequestTest extends PHPUnit\Framework\TestCase
$this->request = new Request(); $this->request = new Request();
} }
protected function tearDown(): void {
unset($_REQUEST);
unset($_SERVER);
}
public function testDefaults() public function testDefaults()
{ {
self::assertEquals('/', $this->request->url); self::assertEquals('/', $this->request->url);
@ -150,4 +155,42 @@ class RequestTest extends PHPUnit\Framework\TestCase
$request = new Request(); $request = new Request();
self::assertEquals('http', $request->scheme); self::assertEquals('http', $request->scheme);
} }
public function testInitUrlSameAsBaseDirectory() {
$request = new Request([
'url' => '/vagrant/public/flightphp',
'base' => '/vagrant/public',
'query' => new Collection(),
'type' => ''
]);
$this->assertEquals('/flightphp', $request->url);
}
public function testInitNoUrl() {
$request = new Request([
'url' => '',
'base' => '/vagrant/public',
'type' => ''
]);
$this->assertEquals('/', $request->url);
}
public function testInitWithJsonBody() {
// create dummy file to pull request body from
$tmpfile = tmpfile();
$stream_path = stream_get_meta_data($tmpfile)['uri'];
file_put_contents($stream_path, '{"foo":"bar"}');
$_SERVER['REQUEST_METHOD'] = 'POST';
$request = new Request([
'url' => '/something/fancy',
'base' => '/vagrant/public',
'type' => 'application/json',
'length' => 13,
'data' => new Collection(),
'query' => new Collection(),
'stream_path' => $stream_path
]);
$this->assertEquals([ 'foo' => 'bar' ], $request->data->getData());
$this->assertEquals('{"foo":"bar"}', $request->getBody());
}
} }

@ -0,0 +1,227 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
use flight\net\Request;
use flight\net\Response;
use flight\util\Collection;
class ResponseTest extends PHPUnit\Framework\TestCase
{
protected function setUp(): void
{
$_SERVER = [];
$_REQUEST = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$_FILES = [];
}
protected function tearDown(): void {
unset($_REQUEST);
unset($_SERVER);
}
public function testStatusDefault() {
$response = new Response();
$this->assertSame(200, $response->status());
}
public function testStatusValidCode() {
$response = new Response();
$response->status(200);
$this->assertEquals(200, $response->status());
}
public function testStatusInvalidCode() {
$response = new Response();
$this->expectException(Exception::class);
$this->expectExceptionMessage('Invalid status code.');
$response->status(999);
}
public function testStatusReturnObject() {
$response = new Response();
$this->assertEquals($response, $response->status(200));
}
public function testHeaderSingle() {
$response = new Response();
$response->header('Content-Type', 'text/html');
$this->assertEquals(['Content-Type' => 'text/html'], $response->headers());
}
public function testHeaderSingleKeepCaseSensitive() {
$response = new Response();
$response->header('content-type', 'text/html');
$response->header('x-test', 'test');
$this->assertEquals(['content-type' => 'text/html', 'x-test' => 'test'], $response->headers());
}
public function testHeaderArray() {
$response = new Response();
$response->header(['Content-Type' => 'text/html', 'X-Test' => 'test']);
$this->assertEquals(['Content-Type' => 'text/html', 'X-Test' => 'test'], $response->headers());
}
public function testHeaderReturnObject() {
$response = new Response();
$this->assertEquals($response, $response->header('Content-Type', 'text/html'));
}
public function testWrite() {
$response = new class extends Response {
public function getBody() {
return $this->body;
}
};
$response->write('test');
$this->assertEquals('test', $response->getBody());
}
public function testWriteEmptyString() {
$response = new class extends Response {
public function getBody() {
return $this->body;
}
};
$response->write('');
$this->assertEquals('', $response->getBody());
}
public function testWriteReturnObject() {
$response = new Response();
$this->assertEquals($response, $response->write('test'));
}
public function testClear() {
$response = new class extends Response {
public function getBody() {
return $this->body;
}
};
$response->write('test');
$response->status(404);
$response->header('Content-Type', 'text/html');
$response->clear();
$this->assertEquals('', $response->getBody());
$this->assertEquals(200, $response->status());
$this->assertEquals([], $response->headers());
}
public function testCacheSimple() {
$response = new Response();
$cache_time = time() + 60;
$response->cache($cache_time);
$this->assertEquals([
'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
'Cache-Control' => 'max-age=60'
], $response->headers());
}
public function testCacheSimpleWithString() {
$response = new Response();
$cache_time = time() + 60;
$response->cache('now +60 seconds');
$this->assertEquals([
'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
'Cache-Control' => 'max-age=60'
], $response->headers());
}
public function testCacheSimpleWithPragma() {
$response = new Response();
$cache_time = time() + 60;
$response->header('Pragma', 'no-cache');
$response->cache($cache_time);
$this->assertEquals([
'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
'Cache-Control' => 'max-age=60'
], $response->headers());
}
public function testCacheFalseExpiresValue() {
$response = new Response();
$response->cache(false);
$this->assertEquals([
'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
'Cache-Control' => [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
],
'Pragma' => 'no-cache'
], $response->headers());
}
public function testSendHeadersRegular() {
$response = new class extends Response {
protected $test_sent_headers = [];
protected array $headers = [
'Cache-Control' => [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
]
];
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
{
$this->test_sent_headers[] = $header_string;
return $this;
}
public function getSentHeaders(): array
{
return $this->test_sent_headers;
}
};
$response->header('Content-Type', 'text/html');
$response->header('X-Test', 'test');
$response->write('Something');
$response->sendHeaders();
$sent_headers = $response->getSentHeaders();
$this->assertEquals([
'HTTP/1.1 200 OK',
'Cache-Control: no-store, no-cache, must-revalidate',
'Cache-Control: post-check=0, pre-check=0',
'Cache-Control: max-age=0',
'Content-Type: text/html',
'X-Test: test',
'Content-Length: 9'
], $sent_headers);
}
public function testSentDefault() {
$response = new Response();
$this->assertFalse($response->sent());
}
public function testSentTrue() {
$response = new class extends Response {
protected $test_sent_headers = [];
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
{
$this->test_sent_headers[] = $header_string;
return $this;
}
};
$response->header('Content-Type', 'text/html');
$response->header('X-Test', 'test');
$response->write('Something');
$this->expectOutputString('Something');
$response->send();
$this->assertTrue($response->sent());
}
}

@ -10,9 +10,6 @@ use flight\core\Dispatcher;
use flight\net\Request; use flight\net\Request;
use flight\net\Router; use flight\net\Router;
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class RouterTest extends PHPUnit\Framework\TestCase class RouterTest extends PHPUnit\Framework\TestCase
{ {
private Router $router; private Router $router;
@ -23,11 +20,18 @@ class RouterTest extends PHPUnit\Framework\TestCase
protected function setUp(): void protected function setUp(): void
{ {
$_SERVER = [];
$_REQUEST = [];
$this->router = new Router(); $this->router = new Router();
$this->request = new Request(); $this->request = new Request();
$this->dispatcher = new Dispatcher(); $this->dispatcher = new Dispatcher();
} }
protected function tearDown(): void {
unset($_REQUEST);
unset($_SERVER);
}
// Simple output // Simple output
public function ok() public function ok()
{ {
@ -101,6 +105,16 @@ class RouterTest extends PHPUnit\Framework\TestCase
$this->check('OK'); $this->check('OK');
} }
// Simple path with trailing slash
// Simple path
public function testPathRouteTrailingSlash()
{
$this->router->map('/path/', [$this, 'ok']);
$this->request->url = '/path';
$this->check('OK');
}
// POST route // POST route
public function testPostRoute() public function testPostRoute()
{ {
@ -293,4 +307,49 @@ class RouterTest extends PHPUnit\Framework\TestCase
$this->check('цветя'); $this->check('цветя');
} }
public function testGetAndClearRoutes() {
$this->router->map('/path1', [$this, 'ok']);
$this->router->map('/path2', [$this, 'ok']);
$this->router->map('/path3', [$this, 'ok']);
$this->router->map('/path4', [$this, 'ok']);
$this->router->map('/path5', [$this, 'ok']);
$this->router->map('/path6', [$this, 'ok']);
$this->router->map('/path7', [$this, 'ok']);
$this->router->map('/path8', [$this, 'ok']);
$this->router->map('/path9', [$this, 'ok']);
$routes = $this->router->getRoutes();
$this->assertEquals(9, count($routes));
$this->router->clear();
$this->assertEquals(0, count($this->router->getRoutes()));
}
public function testResetRoutes() {
$router = new class extends Router {
public function getIndex() {
return $this->index;
}
};
$router->map('/path1', [$this, 'ok']);
$router->map('/path2', [$this, 'ok']);
$router->map('/path3', [$this, 'ok']);
$router->map('/path4', [$this, 'ok']);
$router->map('/path5', [$this, 'ok']);
$router->map('/path6', [$this, 'ok']);
$router->map('/path7', [$this, 'ok']);
$router->map('/path8', [$this, 'ok']);
$router->map('/path9', [$this, 'ok']);
$router->next();
$router->next();
$router->next();
$this->assertEquals(3, $router->getIndex());
$router->reset();
$this->assertEquals(0, $router->getIndex());
}
} }

@ -5,9 +5,6 @@
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com> * @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license * @license MIT, http://flightphp.com/license
*/ */
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class VariableTest extends PHPUnit\Framework\TestCase class VariableTest extends PHPUnit\Framework\TestCase
{ {
/** /**

@ -5,9 +5,6 @@
* @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com> * @copyright Copyright (c) 2012, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license * @license MIT, http://flightphp.com/license
*/ */
require_once 'vendor/autoload.php';
require_once __DIR__ . '/../flight/autoload.php';
class ViewTest extends PHPUnit\Framework\TestCase class ViewTest extends PHPUnit\Framework\TestCase
{ {
/** /**
@ -36,6 +33,21 @@ class ViewTest extends PHPUnit\Framework\TestCase
$this->assertNull($this->view->get('test')); $this->assertNull($this->view->get('test'));
} }
public function testMultipleVariables() {
$this->view->set([
'test' => 123,
'foo' => 'bar'
]);
$this->assertEquals(123, $this->view->get('test'));
$this->assertEquals('bar', $this->view->get('foo'));
$this->view->clear();
$this->assertNull($this->view->get('test'));
$this->assertNull($this->view->get('foo'));
}
// Check if template files exist // Check if template files exist
public function testTemplateExists() public function testTemplateExists()
{ {
@ -51,6 +63,13 @@ class ViewTest extends PHPUnit\Framework\TestCase
$this->expectOutputString('Hello, Bob!'); $this->expectOutputString('Hello, Bob!');
} }
public function testRenderBadFilePath() {
$this->expectException(Exception::class);
$this->expectExceptionMessage('Template file not found: '.__DIR__ . '/views/badfile.php');
$this->view->render('badfile');
}
// Fetch template output // Fetch template output
public function testFetch() public function testFetch()
{ {
@ -79,4 +98,23 @@ class ViewTest extends PHPUnit\Framework\TestCase
$this->expectOutputString('Hello world, Bob!'); $this->expectOutputString('Hello world, Bob!');
} }
public function testGetTemplateAbsolutePath() {
$tmpfile = tmpfile();
$this->view->extension = '';
$file_path = stream_get_meta_data($tmpfile)['uri'];
$this->assertEquals($file_path, $this->view->getTemplate($file_path));
}
public function testE() {
$this->expectOutputString('&lt;script&gt;');
$result = $this->view->e('<script>');
$this->assertEquals('&lt;script&gt;', $result);
}
public function testENoNeedToEscape() {
$this->expectOutputString('script');
$result = $this->view->e('script');
$this->assertEquals('script', $result);
}
} }

@ -0,0 +1,17 @@
<?php
class TesterClass {
public $param1;
public $param2;
public $param3;
public $param4;
public $param5;
public $param6;
public function __construct($param1, $param2, $param3, $param4, $param5, $param6) {
$this->param1 = $param1;
$this->param2 = $param2;
$this->param3 = $param3;
$this->param4 = $param4;
$this->param5 = $param5;
$this->param6 = $param6;
}
};

@ -0,0 +1,6 @@
<?php
$path = file_exists(__DIR__ . '/../vendor/autoload.php')
? __DIR__ . '/../vendor/autoload.php'
: __DIR__ . '/../flight/autoload.php';
require_once($path);
Loading…
Cancel
Save