diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e8d15f4 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index a5ec20f..6014b45 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ vendor/ composer.phar composer.lock +.phpunit.result.cache +coverage/ +.vscode/settings.json +*.sublime-workspace +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 61083e9..daae451 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,28 @@ +![](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? -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. +Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/#/#flight-php-framework:matrix.org) + +# Basic Usage ```php -require 'flight/Flight.php'; -Flight::route('/', function(){ - echo 'hello world!'; +// if installed with composer +require 'vendor/autoload.php'; +// or if installed manually by zip file +//require 'flight/Flight.php'; + +Flight::route('/', function() { + echo 'hello world!'; }); Flight::start(); @@ -27,14 +42,15 @@ Flight is released under the [MIT](http://flightphp.com/license) license. 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: -``` -composer require mikecao/flight +```bash +composer require n0nag0n/flight ``` -OR you can [download](https://github.com/mikecao/flight/archive/master.zip) them directly -and extract them to your web directory. +OR you can [download](https://github.com/n0nag0n/flight/archive/master.zip) +them directly and extract them to your web directory. 2\. Configure your webserver. @@ -47,15 +63,16 @@ RewriteCond %{REQUEST_FILENAME} !-d 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: ``` server { - location / { - try_files $uri $uri/ /index.php; - } + location / { + try_files $uri $uri/ /index.php; + } } ``` 3\. Create your `index.php` file. @@ -75,8 +92,8 @@ require 'vendor/autoload.php'; Then define a route and assign a function to handle the request. ```php -Flight::route('/', function(){ - echo 'hello world!'; +Flight::route('/', function () { + echo 'hello world!'; }); ``` @@ -91,16 +108,16 @@ Flight::start(); Routing in Flight is done by matching a URL pattern with a callback function. ```php -Flight::route('/', function(){ - echo 'hello world!'; +Flight::route('/', function () { + echo 'hello world!'; }); ``` The callback can be any object that is callable. So you can use a regular function: ```php -function hello(){ - echo 'hello world!'; +function hello() { + echo 'hello world!'; } Flight::route('/', 'hello'); @@ -110,9 +127,9 @@ Or a class method: ```php class Greeting { - public static function hello() { - echo 'hello world!'; - } + public static function hello() { + echo 'hello world!'; + } } Flight::route('/', array('Greeting', 'hello')); @@ -123,18 +140,18 @@ Or an object method: ```php class Greeting { - public function __construct() { - $this->name = 'John Doe'; - } + public function __construct() { + $this->name = 'John Doe'; + } - public function hello() { - echo "Hello, {$this->name}!"; - } + public function hello() { + echo "Hello, {$this->name}!"; + } } $greeting = new Greeting(); -Flight::route('/', array($greeting, 'hello')); +Flight::route('/', array($greeting, 'hello')); ``` Routes are matched in the order they are defined. The first route to match a @@ -146,20 +163,20 @@ By default, route patterns are matched against all request methods. You can resp to specific methods by placing an identifier before the URL. ```php -Flight::route('GET /', function(){ - echo 'I received a GET request.'; +Flight::route('GET /', function () { + echo 'I received a GET request.'; }); -Flight::route('POST /', function(){ - echo 'I received a POST request.'; +Flight::route('POST /', function () { + echo 'I received a POST request.'; }); ``` You can also map multiple methods to a single callback by using a `|` delimiter: ```php -Flight::route('GET|POST /', function(){ - echo 'I received either a GET or a POST request.'; +Flight::route('GET|POST /', function () { + echo 'I received either a GET or a POST request.'; }); ``` @@ -168,8 +185,8 @@ Flight::route('GET|POST /', function(){ You can use regular expressions in your routes: ```php -Flight::route('/user/[0-9]+', function(){ - // This will match /user/1234 +Flight::route('/user/[0-9]+', function () { + // This will match /user/1234 }); ``` @@ -179,8 +196,8 @@ You can specify named parameters in your routes which will be passed along to your callback function. ```php -Flight::route('/@name/@id', function($name, $id){ - echo "hello, $name ($id)!"; +Flight::route('/@name/@id', function(string $name, string $id) { + echo "hello, $name ($id)!"; }); ``` @@ -188,9 +205,9 @@ You can also include regular expressions with your named parameters by using the `:` delimiter: ```php -Flight::route('/@name/@id:[0-9]{3}', function($name, $id){ - // This will match /bob/123 - // But will not match /bob/12345 +Flight::route('/@name/@id:[0-9]{3}', function(string $name, string $id) { + // This will match /bob/123 + // 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. ```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: // /blog/2012/12/10 // /blog/2012/12 // /blog/2012 // /blog -}); + } +); ``` Any optional parameters that are not matched will be passed in as NULL. @@ -219,16 +239,16 @@ Matching is only done on individual URL segments. If you want to match multiple segments you can use the `*` wildcard. ```php -Flight::route('/blog/*', function(){ - // This will match /blog/2000/02/01 +Flight::route('/blog/*', function () { + // This will match /blog/2000/02/01 }); ``` To route all requests to a single callback, you can do: ```php -Flight::route('*', function(){ - // Do something +Flight::route('*', function () { + // Do something }); ``` @@ -238,16 +258,16 @@ You can pass execution on to the next matching route by returning `true` from your callback function. ```php -Flight::route('/user/@name', function($name){ - // Check some condition - if ($name != "Bob") { - // Continue to next route - return true; - } +Flight::route('/user/@name', function (string $name) { + // Check some condition + if ($name != "Bob") { + // Continue to next route + return true; + } }); -Flight::route('/user/*', function(){ - // This will get called +Flight::route('/user/*', function () { + // This will get called }); ``` @@ -259,18 +279,18 @@ the route method. The route object will always be the last parameter passed to y callback function. ```php -Flight::route('/', function($route){ - // Array of HTTP methods matched against - $route->methods; +Flight::route('/', function(\flight\net\Route $route) { + // Array of HTTP methods matched against + $route->methods; - // Array of named parameters - $route->params; + // Array of named parameters + $route->params; - // Matching regular expression - $route->regex; + // Matching regular expression + $route->regex; - // Contains the contents of any '*' used in the URL pattern - $route->splat; + // Contains the contents of any '*' used in the URL pattern + $route->splat; }, true); ``` @@ -286,8 +306,8 @@ To map your own custom method, you use the `map` function: ```php // Map your method -Flight::map('hello', function($name){ - echo "hello $name!"; +Flight::map('hello', function ($name) { + echo "hello $name!"; }); // Call your custom method @@ -318,7 +338,7 @@ Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','p // Get an instance of your class // This will create an object with the defined parameters // -// new PDO('mysql:host=localhost;dbname=test','user','pass'); +// new PDO('mysql:host=localhost;dbname=test','user','pass'); // $db = Flight::db(); ``` @@ -329,8 +349,8 @@ new object. The callback function takes one parameter, an instance of the new ob ```php // The callback will be passed the object that was constructed -Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function($db){ - $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function (PDO $db): void { + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); }); ``` @@ -359,8 +379,8 @@ by using the `map` method: ```php Flight::map('notFound', function(){ - // Display custom 404 page - include 'errors/404.html'; + // Display custom 404 page + include 'errors/404.html'; }); ``` @@ -387,8 +407,8 @@ methods as well as any custom methods that you've mapped. A filter function looks like this: ```php -function(&$params, &$output) { - // Filter code +function (array &$params, string &$output) { + // Filter code } ``` @@ -397,16 +417,16 @@ 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: ```php -Flight::before('start', function(&$params, &$output){ - // Do something +Flight::before('start', function (array &$params, string &$output) { + // Do something }); ``` You can have a filter run after a method by doing: ```php -Flight::after('start', function(&$params, &$output){ - // Do something +Flight::after('start', function (array &$params, string &$output) { + // Do something }); ``` @@ -417,20 +437,20 @@ Here's an example of the filtering process: ```php // Map a custom method -Flight::map('hello', function($name){ - return "Hello, $name!"; +Flight::map('hello', function ($name) { + return "Hello, $name!"; }); // Add a before filter -Flight::before('hello', function(&$params, &$output){ - // Manipulate the parameter - $params[0] = 'Fred'; +Flight::before('hello', function (array &$params, string &$output) { + // Manipulate the parameter + $params[0] = 'Fred'; }); // Add an after filter -Flight::after('hello', function(&$params, &$output){ - // Manipulate the output - $output .= " Have a nice day!"; +Flight::after('hello', function (array &$params, string &$output) { + // Manipulate the output + $output .= " Have a nice day!"; }); // Invoke the custom method @@ -439,26 +459,28 @@ echo Flight::hello('Bob'); 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` in any of your filter functions: ```php -Flight::before('start', function(&$params, &$output){ - echo 'one'; +Flight::before('start', function (array &$params, string &$output){ + echo 'one'; }); -Flight::before('start', function(&$params, &$output){ - echo 'two'; +Flight::before('start', function (array &$params, string &$output): bool { + echo 'two'; - // This will end the chain - return false; + // This will end the chain + return false; }); // This will not get called -Flight::before('start', function(&$params, &$output){ - echo 'three'; +Flight::before('start', function (array &$params, string &$output){ + echo 'three'; }); ``` @@ -480,7 +502,7 @@ To see if a variable has been set you can do: ```php if (Flight::has('id')) { - // Do something + // Do something } ``` @@ -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: ```php -Hello, ''! +Hello, ! ``` The output would be: - Hello, Bob! +``` +Hello, Bob! +``` You can also manually set view variables by using the set method: @@ -580,26 +604,26 @@ If the template files looks like this: ```php - -<?php echo $title; ?> - - - - - + + <?php echo $title; ?> + + + + + ``` The output would be: ```html - -Home Page - - -

Hello

-
World
- + + Home Page + + +

Hello

+
World
+ ``` @@ -615,11 +639,11 @@ require './Smarty/libs/Smarty.class.php'; // Register Smarty as the view class // Also pass a callback function to configure Smarty on load -Flight::register('view', 'Smarty', array(), function($smarty){ - $smarty->template_dir = './templates/'; - $smarty->compile_dir = './templates_c/'; - $smarty->config_dir = './config/'; - $smarty->cache_dir = './cache/'; +Flight::register('view', 'Smarty', array(), function (Smarty $smarty) { + $smarty->setTemplateDir() = './templates/'; + $smarty->setCompileDir() = './templates_c/'; + $smarty->setConfigDir() = './config/'; + $smarty->setCacheDir() = './cache/'; }); // Assign template data @@ -633,8 +657,8 @@ For completeness, you should also override Flight's default render method: ```php Flight::map('render', function($template, $data){ - Flight::view()->assign($data); - Flight::view()->display($template); + Flight::view()->assign($data); + Flight::view()->display($template); }); ``` # Error Handling @@ -648,9 +672,9 @@ response with some error information. You can override this behavior for your own needs: ```php -Flight::map('error', function(Exception $ex){ - // Handle error - echo $ex->getTraceAsString(); +Flight::map('error', function(Throwable $ex){ + // Handle error + echo $ex->getTraceAsString(); }); ``` @@ -669,8 +693,8 @@ behavior is to send an `HTTP 404 Not Found` response with a simple message. You can override this behavior for your own needs: ```php -Flight::map('notFound', function(){ - // Handle not found +Flight::map('notFound', function () { + // Handle not found }); ``` @@ -701,26 +725,24 @@ $request = Flight::request(); The request object provides the following properties: -``` -url - The URL being requested -base - The parent subdirectory of the URL -method - The request method (GET, POST, PUT, DELETE) -referrer - The referrer URL -ip - IP address of the client -ajax - Whether the request is an AJAX request -scheme - The server protocol (http, https) -user_agent - Browser information -type - The content type -length - The content length -query - Query string parameters -data - Post data or JSON data -cookies - Cookie data -files - Uploaded files -secure - Whether the connection is secure -accept - HTTP accept parameters -proxy_ip - Proxy IP address of the client -host - The request host name -``` +- **url** - The URL being requested +- **base** - The parent subdirectory of the URL +- **method** - The request method (GET, POST, PUT, DELETE) +- **referrer** - The referrer URL +- **ip** - IP address of the client +- **ajax** - Whether the request is an AJAX request +- **scheme** - The server protocol (http, https) +- **user_agent** - Browser information +- **type** - The content type +- **length** - The content length +- **query** - Query string parameters +- **data** - Post data or JSON data +- **cookies** - Cookie data +- **files** - Uploaded files +- **secure** - Whether the connection is secure +- **accept** - HTTP accept parameters +- **proxy_ip** - Proxy IP address of the client +- **host** - The request host name You can access the `query`, `data`, `cookies`, and `files` properties as arrays or objects. @@ -739,7 +761,8 @@ $id = Flight::request()->query->id; ## 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 $body = Flight::request()->getBody(); @@ -747,8 +770,8 @@ $body = Flight::request()->getBody(); ## JSON Input -If you send a request with the type `application/json` and the data `{"id": 123}` it will be available -from the `data` property: +If you send a request with the type `application/json` and the data `{"id": 123}` +it will be available from the `data` property: ```php $id = Flight::request()->data->id; @@ -768,9 +791,9 @@ and time a page was last modified. The client will continue to use their cache u the last modified value is changed. ```php -Flight::route('/news', function(){ - Flight::lastModified(1234567890); - echo 'This content will be cached.'; +Flight::route('/news', function () { + Flight::lastModified(1234567890); + echo 'This content will be cached.'; }); ``` @@ -780,9 +803,9 @@ Flight::route('/news', function(){ want for the resource: ```php -Flight::route('/news', function(){ - Flight::etag('my-unique-id'); - echo 'This content will be cached.'; +Flight::route('/news', function () { + Flight::etag('my-unique-id'); + echo 'This content will be cached.'; }); ``` @@ -847,12 +870,12 @@ Flight::set('flight.log_errors', true); The following is a list of all the available configuration settings: - flight.base_url - Override the base url of the request. (default: null) - flight.case_sensitive - Case sensitive matching for URLs. (default: false) - 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.views.path - Directory containing view template files. (default: ./views) - flight.views.extension - View template file extension. (default: .php) +- **flight.base_url** - Override the base url of the request. (default: null) +- **flight.case_sensitive** - Case sensitive matching for URLs. (default: false) +- **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.views.path** - Directory containing view template files. (default: ./views) +- **flight.views.extension** - View template file extension. (default: .php) # Framework Methods @@ -864,15 +887,15 @@ or overridden. ## Core Methods ```php -Flight::map($name, $callback) // Creates a custom framework method. -Flight::register($name, $class, [$params], [$callback]) // Registers a class to a framework method. -Flight::before($name, $callback) // Adds a filter before a framework method. -Flight::after($name, $callback) // Adds a filter after a framework method. -Flight::path($path) // Adds a path for autoloading classes. -Flight::get($key) // Gets a variable. -Flight::set($key, $value) // Sets a variable. -Flight::has($key) // Checks if a variable is set. -Flight::clear([$key]) // Clears a variable. +Flight::map(string $name, callable $callback, bool $pass_route = false) // Creates a custom framework method. +Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registers a class to a framework method. +Flight::before(string $name, callable $callback) // Adds a filter before a framework method. +Flight::after(string $name, callable $callback) // Adds a filter after a framework method. +Flight::path(string $path) // Adds a path for autoloading classes. +Flight::get(string $key) // Gets a variable. +Flight::set(string $key, mixed $value) // Sets a variable. +Flight::has(string $key) // Checks if a variable is set. +Flight::clear(array|string $key = []) // Clears a variable. Flight::init() // Initializes the framework to its default settings. Flight::app() // Gets the application object instance ``` @@ -882,16 +905,16 @@ Flight::app() // Gets the application object instance ```php Flight::start() // Starts the framework. Flight::stop() // Stops the framework and sends a response. -Flight::halt([$code], [$message]) // Stop the framework with an optional status code and message. -Flight::route($pattern, $callback) // Maps a URL pattern to a callback. -Flight::redirect($url, [$code]) // Redirects to another URL. -Flight::render($file, [$data], [$key]) // Renders a template file. -Flight::error($exception) // Sends an HTTP 500 response. +Flight::halt(int $code = 200, string $message = '') // Stop the framework with an optional status code and message. +Flight::route(string $pattern, callable $callback, bool $pass_route = false) // Maps a URL pattern to a callback. +Flight::redirect(string $url, int $code) // Redirects to another URL. +Flight::render(string $file, array $data, ?string $key = null) // Renders a template file. +Flight::error(Throwable $exception) // Sends an HTTP 500 response. Flight::notFound() // Sends an HTTP 404 response. -Flight::etag($id, [$type]) // Performs ETag HTTP caching. -Flight::lastModified($time) // Performs last modified HTTP caching. -Flight::json($data, [$code], [$encode], [$charset], [$option]) // Sends a JSON response. -Flight::jsonp($data, [$param], [$code], [$encode], [$charset], [$option]) // Sends a JSONP response. +Flight::etag(string $id, string $type = 'string') // Performs ETag HTTP caching. +Flight::lastModified(int $time) // Performs last modified HTTP caching. +Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON 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. @@ -905,12 +928,10 @@ as an object instance. ```php require 'flight/autoload.php'; -use flight\Engine; +$app = new flight\Engine(); -$app = new Engine(); - -$app->route('/', function(){ - echo 'hello world!'; +$app->route('/', function () { + echo 'hello world!'; }); $app->start(); diff --git a/VERSION b/VERSION index 38f77a6..4a36342 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 +3.0.0 diff --git a/composer.json b/composer.json index 8e2611b..2e0459c 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "mikecao/flight", - "description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications.", + "name": "flightphp/core", + "description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications. This is the maintained fork of mikecao/flight", "homepage": "http://flightphp.com", "license": "MIT", "authors": [ @@ -9,16 +9,52 @@ "email": "mike@mikecao.com", "homepage": "http://www.mikecao.com/", "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": { - "php": "^7.4|^8.0|^8.1", + "php": "^7.4|^8.0|^8.1|^8.2", "ext-json": "*" }, "autoload": { - "files": [ "flight/autoload.php", "flight/Flight.php" ] + "files": [ + "flight/autoload.php", + "flight/Flight.php" + ] }, "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" + } } diff --git a/flight.sublime-project b/flight.sublime-project new file mode 100644 index 0000000..26552b2 --- /dev/null +++ b/flight.sublime-project @@ -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" + }, + }, + }, +} diff --git a/flight/Engine.php b/flight/Engine.php index f8c996b..4b1bdca 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -30,7 +30,14 @@ use Throwable; * @method void start() Starts engine * @method void stop() Stops framework and outputs current 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 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 * * Views @@ -40,7 +47,7 @@ use Throwable; * Request-response * @method Request request() Gets current request * @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 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. @@ -54,6 +61,7 @@ class Engine { /** * Stored variables. + * @var array */ protected array $vars; @@ -67,6 +75,13 @@ class Engine */ protected Dispatcher $dispatcher; + /** + * If the framework has been initialized or not + * + * @var boolean + */ + protected bool $initialized = false; + /** * Constructor. */ @@ -84,7 +99,7 @@ class Engine * Handles calls to class methods. * * @param string $name Method name - * @param array $params Method parameters + * @param array $params Method parameters * * @throws Exception * @@ -114,7 +129,7 @@ class Engine */ public function init(): void { - static $initialized = false; + $initialized = $this->initialized; $self = $this; if ($initialized) { @@ -155,8 +170,8 @@ class Engine $this->before('start', function () use ($self) { // Enable error handling if ($self->get('flight.handle_errors')) { - set_error_handler([$self, 'handleError']); - set_exception_handler([$self, 'handleException']); + set_error_handler(array($self, 'handleError')); + set_exception_handler(array($self, 'handleException')); } // Set case-sensitivity @@ -165,7 +180,7 @@ class Engine $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 * * @throws ErrorException + * @return bool */ public function handleError(int $errno, string $errstr, string $errfile, int $errline) { if ($errno & error_reporting()) { throw new ErrorException($errstr, $errno, 0, $errfile, $errline); } + + return false; } /** * Custom exception handler. Logs exceptions. * - * @param Exception $e Thrown exception + * @param Throwable $e Thrown exception */ public function handleException($e): void { if ($this->get('flight.log_errors')) { - error_log($e->getMessage()); + error_log($e->getMessage()); // @codeCoverageIgnore } $this->error($e); @@ -203,7 +221,7 @@ class Engine * Maps a callback to a framework method. * * @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 */ @@ -218,11 +236,12 @@ class Engine /** * Registers a class to a framework method. + * @template T of object * * @param string $name Method name - * @param string $class Class name - * @param array $params Class initialization parameters - * @param callable|null $callback $callback Function to call after object instantiation + * @param class-string $class Class name + * @param array $params Class initialization parameters + * @param ?callable(T $instance): void $callback Function to call after object instantiation * * @throws Exception If trying to map over a framework method */ @@ -239,7 +258,7 @@ class Engine * Adds a pre-filter to a method. * * @param string $name Method name - * @param callback $callback Callback function + * @param callable $callback Callback function */ public function before(string $name, callable $callback): void { @@ -250,7 +269,7 @@ class Engine * Adds a post-filter to a method. * * @param string $name Method name - * @param callback $callback Callback function + * @param callable $callback Callback function */ public function after(string $name, callable $callback): void { @@ -348,7 +367,7 @@ class Engine // Flush any existing output if (ob_get_length() > 0) { - $response->write(ob_get_clean()); + $response->write(ob_get_clean()); // @codeCoverageIgnore } // Enable output buffering @@ -392,9 +411,10 @@ class Engine */ public function _error($e): void { - $msg = sprintf('

500 Internal Server Error

' . - '

%s (%s)

' . - '
%s
', + $msg = sprintf( + '

500 Internal Server Error

' . + '

%s (%s)

' . + '
%s
', $e->getMessage(), $e->getCode(), $e->getTraceAsString() @@ -406,9 +426,11 @@ class Engine ->status(500) ->write($msg) ->send(); + // @codeCoverageIgnoreStart } catch (Throwable $t) { exit($msg); } + // @codeCoverageIgnoreEnd } /** @@ -427,7 +449,8 @@ class Engine $response->status($code); } - $response->write(ob_get_clean()); + $content = ob_get_clean(); + $response->write($content ?: ''); $response->send(); } @@ -437,7 +460,7 @@ class Engine * Routes a URL to a callback function. * * @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 */ 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. * * @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 */ 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. * * @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 */ 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. * * @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 */ 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. * * @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 */ 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 string $message Response message + * */ public function _halt(int $code = 200, string $message = ''): void { @@ -506,7 +530,10 @@ class Engine ->status($code) ->write($message) ->send(); - exit(); + // apologies for the crappy hack here... + if($message !== 'skip---exit') { + exit(); // @codeCoverageIgnore + } } /** @@ -519,8 +546,8 @@ class Engine ->status(404) ->write( '

404 Not Found

' . - '

The page you have requested could not be found.

' . - str_repeat(' ', 512) + '

The page you have requested could not be found.

' . + str_repeat(' ', 512) ) ->send(); } @@ -555,7 +582,7 @@ class Engine * Renders a template. * * @param string $file Template file - * @param array|null $data Template data + * @param ?array $data Template data * @param string|null $key View variable name * * @throws Exception @@ -639,8 +666,10 @@ class Engine $this->response()->header('ETag', $id); - if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && - $_SERVER['HTTP_IF_NONE_MATCH'] === $id) { + if ( + isset($_SERVER['HTTP_IF_NONE_MATCH']) && + $_SERVER['HTTP_IF_NONE_MATCH'] === $id + ) { $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)); - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && - strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) { + if ( + isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && + strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time + ) { $this->halt(304); } } diff --git a/flight/Flight.php b/flight/Flight.php index 101c455..69a8b65 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -18,36 +18,27 @@ use flight\template\View; /** * The Flight class is a static representation of the framework. * - * Core. - * * @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 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($pattern, $callback) Maps a URL pattern to a callback. + * @method static void route(string $pattern, callable $callback, bool $pass_route = false) Maps a URL pattern to a callback. * @method static Router router() Returns Router instance. * - * Extending & Overriding. - * @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. + * @method static void map(string $name, callable $callback) Creates a custom framework method. * - * Filtering. * @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. * - * Variables. * @method static void set($key, $value) Sets a variable. * @method static mixed get($key) Gets a variable. * @method static bool has($key) Checks if a variable is set. * @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 View view() Returns View instance. * - * Request & Response. * @method static Request request() Returns Request instance. * @method static Response response() Returns Response instance. * @method static void redirect($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 notFound() Sends an HTTP 404 response. * - * HTTP Caching. * @method static void etag($id, $type = 'strong') Performs ETag HTTP caching. * @method static void lastModified($time) Performs last modified HTTP caching. */ @@ -67,24 +57,50 @@ class Flight */ private static Engine $engine; - // Don't allow object instantiation + /** + * Don't allow object instantiation + * + * @codeCoverageIgnore + * @return void + */ 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 $class Fully Qualified Class Name + * @param array $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. * * @param string $name Method name - * @param array $params Method parameters + * @param array $params Method parameters * * @throws Exception * diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 50b73f0..4ce6854 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -23,11 +23,13 @@ class Dispatcher { /** * Mapped events. + * @var array */ protected array $events = []; /** * Method filters. + * @var array>> */ protected array $filters = []; @@ -35,9 +37,9 @@ class Dispatcher * Dispatches an event. * * @param string $name Event name - * @param array $params Callback parameters + * @param array $params Callback parameters * - *@throws Exception + * @throws Exception * * @return mixed|null Output of callback */ @@ -65,7 +67,7 @@ class Dispatcher * Assigns a callback to an event. * * @param string $name Event name - * @param callback $callback Callback function + * @param callable $callback Callback function */ final public function set(string $name, callable $callback): void { @@ -77,7 +79,7 @@ class Dispatcher * * @param string $name Event name * - * @return callback $callback Callback function + * @return callable $callback Callback function */ final public function get(string $name): ?callable { @@ -118,7 +120,7 @@ class Dispatcher * * @param string $name Event name * @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 { @@ -128,8 +130,8 @@ class Dispatcher /** * Executes a chain of method filters. * - * @param array $filters Chain of filters - * @param array $params Method parameters + * @param array $filters Chain of filters + * @param array $params Method parameters * @param mixed $output Method output * * @throws Exception @@ -148,10 +150,10 @@ class Dispatcher /** * Executes a callback function. * - * @param array|callback $callback Callback function - * @param array $params Function parameters + * @param callable|array $callback Callback function + * @param array $params Function parameters * - *@throws Exception + * @throws Exception * * @return mixed Function results */ @@ -170,7 +172,7 @@ class Dispatcher * Calls a function. * * @param callable|string $func Name of function to call - * @param array $params Function parameters + * @param array $params Function parameters * * @return mixed Function results */ @@ -203,7 +205,7 @@ class Dispatcher * Invokes a method. * * @param mixed $func Class method - * @param array $params Class method parameters + * @param array $params Class method parameters * * @return mixed Function results */ @@ -230,6 +232,8 @@ class Dispatcher return ($instance) ? $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: return ($instance) ? $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]); default: return \call_user_func_array($func, $params); + // @codeCoverageIgnoreEnd } } diff --git a/flight/core/Loader.php b/flight/core/Loader.php index 95874a6..c438445 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace flight\core; +use Closure; use Exception; use ReflectionClass; use ReflectionException; @@ -24,26 +25,30 @@ class Loader { /** * Registered classes. + * @var array, ?callable}> $classes */ protected array $classes = []; /** * Class instances. + * @var array */ protected array $instances = []; /** * Autoload directories. + * @var array */ protected static array $dirs = []; /** * Registers a class. + * @template T of object * * @param string $name Registry name - * @param callable|string $class Class name or function to instantiate class - * @param array $params Class initialization parameters - * @param callable|null $callback $callback Function to call after object instantiation + * @param class-string $class Class name or function to instantiate class + * @param array $params Class initialization parameters + * @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 { @@ -77,7 +82,7 @@ class Loader $obj = null; 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]); @@ -116,15 +121,16 @@ class Loader /** * Gets a new instance of a class. + * @template T of object * - * @param callable|string $class Class name or callback function to instantiate class - * @param array $params Class initialization parameters + * @param class-string|Closure(): class-string $class Class name or callback function to instantiate class + * @param array $params Class initialization parameters * * @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)) { return \call_user_func_array($class, $params); @@ -135,6 +141,7 @@ class Loader return new $class(); case 1: return new $class($params[0]); + // @codeCoverageIgnoreStart case 2: return new $class($params[0], $params[1]); case 3: @@ -143,6 +150,7 @@ class Loader return new $class($params[0], $params[1], $params[2], $params[3]); case 5: return new $class($params[0], $params[1], $params[2], $params[3], $params[4]); + // @codeCoverageIgnoreEnd default: try { $refClass = new ReflectionClass($class); @@ -179,14 +187,14 @@ class Loader * Starts/stops autoloader. * * @param bool $enabled Enable/disable autoloading - * @param mixed $dirs Autoload directories + * @param string|iterable $dirs Autoload directories */ public static function autoload(bool $enabled = true, $dirs = []): void { if ($enabled) { spl_autoload_register([__CLASS__, 'loadClass']); } else { - spl_autoload_unregister([__CLASS__, 'loadClass']); + spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore } if (!empty($dirs)) { @@ -216,7 +224,7 @@ class Loader /** * Adds a directory for autoloading classes. * - * @param mixed $dir Directory path + * @param string|iterable $dir Directory path */ public static function addDirectory($dir): void { diff --git a/flight/net/Request.php b/flight/net/Request.php index 75d4665..8a40a50 100644 --- a/flight/net/Request.php +++ b/flight/net/Request.php @@ -18,23 +18,24 @@ use flight\util\Collection; * are stored and accessible via the Request object. * * The default request properties are: - * url - The URL being requested - * base - The parent subdirectory of the URL - * method - The request method (GET, POST, PUT, DELETE) - * referrer - The referrer URL - * ip - IP address of the client - * ajax - Whether the request is an AJAX request - * scheme - The server protocol (http, https) - * user_agent - Browser information - * type - The content type - * length - The content length - * query - Query string parameters - * data - Post parameters - * cookies - Cookie parameters - * files - Uploaded files - * secure - Connection is secure - * accept - HTTP accept parameters - * proxy_ip - Proxy IP address of the client + * + * - **url** - The URL being requested + * - **base** - The parent subdirectory of the URL + * - **method** - The request method (GET, POST, PUT, DELETE) + * - **referrer** - The referrer URL + * - **ip** - IP address of the client + * - **ajax** - Whether the request is an AJAX request + * - **scheme** - The server protocol (http, https) + * - **user_agent** - Browser information + * - **type** - The content type + * - **length** - The content length + * - **query** - Query string parameters + * - **data** - Post parameters + * - **cookies** - Cookie parameters + * - **files** - Uploaded files + * - **secure** - Connection is secure + * - **accept** - HTTP accept parameters + * - **proxy_ip** - Proxy IP address of the client */ final class Request { @@ -128,12 +129,25 @@ final class Request */ 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. * - * @param array $config Request configuration + * @param array $config Request configuration + * @param string */ - public function __construct(array $config = []) + public function __construct($config = array()) { // Default properties if (empty($config)) { @@ -165,7 +179,8 @@ final class Request /** * Initialize request properties. * - * @param array $properties Array of request properties + * @param array $properties Array of request properties + * @return static */ public function init(array $properties = []) { @@ -175,6 +190,9 @@ final class Request } // 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)) { $this->url = substr($this->url, \strlen($this->base)); } @@ -191,7 +209,7 @@ final class Request // Check for JSON input if (0 === strpos($this->type, 'application/json')) { - $body = self::getBody(); + $body = $this->getBody(); if ('' !== $body && null !== $body) { $data = json_decode($body, true); 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 */ - public static function getBody(): ?string + public function getBody(): ?string { - static $body; + $body = $this->body; - if (null !== $body) { + if ('' !== $body) { return $body; } $method = self::getMethod(); 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; } @@ -287,11 +309,11 @@ final class Request * * @param string $url URL string * - * @return array Query parameters + * @return array> */ public static function parseQuery(string $url): array { - $params = []; + $params = array(); $args = parse_url($url); if (isset($args['query'])) { diff --git a/flight/net/Response.php b/flight/net/Response.php index e21fc9a..38d06ec 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -25,7 +25,7 @@ class Response public bool $content_length = true; /** - * @var array HTTP status codes + * @var array HTTP status codes */ public static array $codes = [ 100 => 'Continue', @@ -103,7 +103,7 @@ class Response protected int $status = 200; /** - * @var array HTTP headers + * @var array> HTTP headers */ protected array $headers = []; @@ -124,7 +124,7 @@ class Response * * @throws Exception If invalid status code * - * @return int|object Self reference + * @return int|static Self reference */ public function status(?int $code = null) { @@ -144,10 +144,10 @@ class Response /** * Adds a header to the response. * - * @param array|string $name Header name or array of names and values + * @param array|string $name Header name or array of names and values * @param string|null $value Header value * - * @return object Self reference + * @return static Self reference */ public function header($name, ?string $value = null) { @@ -164,8 +164,7 @@ class Response /** * Returns the headers from the response. - * - * @return array + * @return array> */ public function headers() { @@ -203,7 +202,7 @@ class 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 */ @@ -238,7 +237,8 @@ class Response { // Send status code header if (false !== strpos(\PHP_SAPI, 'cgi')) { - header( + // @codeCoverageIgnoreStart + $this->setRealHeader( sprintf( 'Status: %d %s', $this->status, @@ -246,13 +246,15 @@ class Response ), true ); + // @codeCoverageIgnoreEnd } else { - header( + $this->setRealHeader( sprintf( '%s %d %s', $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1', $this->status, - self::$codes[$this->status]), + self::$codes[$this->status] + ), true, $this->status ); @@ -262,10 +264,10 @@ class Response foreach ($this->headers as $field => $value) { if (\is_array($value)) { foreach ($value as $v) { - header($field . ': ' . $v, false); + $this->setRealHeader($field . ': ' . $v, false); } } else { - header($field . ': ' . $value); + $this->setRealHeader($field . ': ' . $value); } } @@ -274,13 +276,27 @@ class Response $length = $this->getContentLength(); if ($length > 0) { - header('Content-Length: ' . $length); + $this->setRealHeader('Content-Length: ' . $length); } } 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. * @@ -294,7 +310,7 @@ class Response } /** - * Gets whether response was sent. + * Gets whether response body was sent. */ public function sent(): bool { @@ -307,11 +323,11 @@ class Response public function send(): void { if (ob_get_length() > 0) { - ob_end_clean(); + ob_end_clean(); // @codeCoverageIgnore } if (!headers_sent()) { - $this->sendHeaders(); + $this->sendHeaders(); // @codeCoverageIgnore } echo $this->body; diff --git a/flight/net/Route.php b/flight/net/Route.php index 91a6aff..fbfc20b 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -28,12 +28,12 @@ final class Route public $callback; /** - * @var array HTTP methods + * @var array HTTP methods */ public array $methods = []; /** - * @var array Route parameters + * @var array Route parameters */ public array $params = []; @@ -56,8 +56,8 @@ final class Route * Constructor. * * @param string $pattern URL pattern - * @param mixed $callback Callback function - * @param array $methods HTTP methods + * @param callable $callback Callback function + * @param array $methods HTTP methods * @param bool $pass Pass self in callback parameters */ public function __construct(string $pattern, $callback, array $methods, bool $pass) diff --git a/flight/net/Router.php b/flight/net/Router.php index dcf1591..f1f8842 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -23,6 +23,7 @@ class Router public bool $case_sensitive = false; /** * Mapped routes. + * @var array */ protected array $routes = []; @@ -34,7 +35,7 @@ class Router /** * Gets mapped routes. * - * @return array Array of routes + * @return array Array of routes */ public function getRoutes(): array { @@ -53,7 +54,7 @@ class Router * Maps a URL pattern to a callback function. * * @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 */ public function map(string $pattern, callable $callback, bool $pass_route = false): void @@ -81,7 +82,7 @@ class Router { $url_decoded = urldecode($request->url); 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; } $this->next(); diff --git a/flight/template/View.php b/flight/template/View.php index 90dbb81..818a100 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -34,7 +34,7 @@ class View /** * View variables. * - * @var array + * @var array */ protected $vars = []; @@ -70,8 +70,9 @@ class View /** * Sets a template variable. * - * @param mixed $key Key - * @param string $value Value + * @param string|iterable $key Key + * @param mixed $value Value + * @return static */ public function set($key, $value = null) { @@ -82,6 +83,8 @@ class View } else { $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. * * @param string $key Key + * @return static */ public function clear($key = null) { @@ -108,15 +112,18 @@ class View } else { unset($this->vars[$key]); } + + return $this; } /** * Renders a template. * * @param string $file Template file - * @param array $data Template data + * @param array $data Template data * * @throws \Exception If template not found + * @return void */ public function render($file, $data = null) { @@ -139,7 +146,7 @@ class View * Gets the output of a template. * * @param string $file Template file - * @param array $data Template data + * @param array $data Template data * * @return string Output of template */ @@ -179,11 +186,13 @@ class View $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 $this->path . '/' . $file; + return $this->path . DIRECTORY_SEPARATOR . $file; } /** @@ -195,6 +204,8 @@ class View */ public function e($str) { - echo htmlentities($str); + $value = htmlentities($str); + echo $value; + return $value; } } diff --git a/flight/util/Collection.php b/flight/util/Collection.php index 97658ec..01ddb81 100644 --- a/flight/util/Collection.php +++ b/flight/util/Collection.php @@ -11,30 +11,32 @@ declare(strict_types=1); namespace flight\util; use ArrayAccess; -use function count; use Countable; use Iterator; use 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 * using both array and object notation. + * @implements ArrayAccess + * @implements Iterator */ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable { /** * Collection data. + * @var array */ private array $data; /** * Constructor. * - * @param array $data Initial data + * @param array $data Initial data */ public function __construct(array $data = []) { @@ -102,11 +104,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ /** * Sets an item at the offset. * - * @param string $offset Offset + * @param ?string $offset Offset * @param mixed $value Value */ #[\ReturnTypeWillChange] - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->data[] = $value; @@ -169,13 +171,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ /** * Gets the next collection value. - * - * @return mixed Value */ #[\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); - return null !== $key && false !== $key; + return null !== $key; } /** @@ -203,7 +203,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ /** * Gets the item keys. * - * @return array Collection keys + * @return array Collection keys */ public function keys(): array { @@ -213,7 +213,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ /** * Gets the collection data. * - * @return array Collection data + * @return array Collection data */ public function getData(): array { @@ -223,19 +223,15 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ /** * Sets the collection data. * - * @param array $data New collection data + * @param array $data New collection data */ public function setData(array $data): void { $this->data = $data; } - /** - * Gets the collection data which can be serialized to JSON. - * - * @return array Collection data which can be serialized by json_encode - */ - public function jsonSerialize(): array + #[\ReturnTypeWillChange] + public function jsonSerialize() { return $this->data; } diff --git a/flight/util/LegacyJsonSerializable.php b/flight/util/LegacyJsonSerializable.php index 0c973aa..39e1721 100644 --- a/flight/util/LegacyJsonSerializable.php +++ b/flight/util/LegacyJsonSerializable.php @@ -9,5 +9,9 @@ declare(strict_types=1); */ interface LegacyJsonSerializable { + /** + * Gets the collection data which can be serialized to JSON. + * @return mixed Collection data which can be serialized by json_encode + */ public function jsonSerialize(); } diff --git a/flight/util/ReturnTypeWillChange.php b/flight/util/ReturnTypeWillChange.php new file mode 100644 index 0000000..3ed841b --- /dev/null +++ b/flight/util/ReturnTypeWillChange.php @@ -0,0 +1,3 @@ + + + + + flight/ + + + + + tests/ + + + + diff --git a/tests/AutoloadTest.php b/tests/AutoloadTest.php index 0fbb958..993659b 100644 --- a/tests/AutoloadTest.php +++ b/tests/AutoloadTest.php @@ -8,9 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class AutoloadTest extends PHPUnit\Framework\TestCase { /** diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php new file mode 100644 index 0000000..8d6ba76 --- /dev/null +++ b/tests/CollectionTest.php @@ -0,0 +1,98 @@ + + * @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()); + } +} diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 63421a4..0fc5d47 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -8,9 +8,6 @@ use flight\core\Dispatcher; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/classes/Hello.php'; - class DispatcherTest extends PHPUnit\Framework\TestCase { /** @@ -47,6 +44,61 @@ class DispatcherTest extends PHPUnit\Framework\TestCase 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 public function testClassMethodMapping() { @@ -98,4 +150,31 @@ class DispatcherTest extends PHPUnit\Framework\TestCase $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); + } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php new file mode 100644 index 0000000..cf2b13a --- /dev/null +++ b/tests/EngineTest.php @@ -0,0 +1,268 @@ + + * @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('

500 Internal Server Error

thrown exception message (20)

'); + $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('

404 Not Found

The page you have requested could not be found.

'); + $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()); + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 137bb61..9681a62 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -8,9 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class FilterTest extends PHPUnit\Framework\TestCase { /** diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 19dfdd3..588ebb2 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -11,16 +11,20 @@ use flight\template\View; * @copyright Copyright (c) 2012, Mike Cao * @license MIT, http://flightphp.com/license */ -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/Flight.php'; - class FlightTest extends PHPUnit\Framework\TestCase { protected function setUp(): void { + $_SERVER = []; + $_REQUEST = []; Flight::init(); } + protected function tearDown(): void { + unset($_REQUEST); + unset($_SERVER); + } + // Checks that default components are loaded public function testDefaultComponents() { diff --git a/tests/LoaderTest.php b/tests/LoaderTest.php index 2e2de8f..ecadd0a 100644 --- a/tests/LoaderTest.php +++ b/tests/LoaderTest.php @@ -8,9 +8,9 @@ use flight\core\Loader; -require_once 'vendor/autoload.php'; require_once __DIR__ . '/classes/User.php'; require_once __DIR__ . '/classes/Factory.php'; +require_once __DIR__ . '/classes/TesterClass.php'; class LoaderTest extends PHPUnit\Framework\TestCase { @@ -118,4 +118,42 @@ class LoaderTest extends PHPUnit\Framework\TestCase self::assertIsObject($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()); + } } diff --git a/tests/MapTest.php b/tests/MapTest.php index 5b0d3df..4f4a0eb 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -8,8 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; require_once __DIR__ . '/classes/Hello.php'; class MapTest extends PHPUnit\Framework\TestCase diff --git a/tests/RedirectTest.php b/tests/RedirectTest.php index 0f8079a..9099fe4 100644 --- a/tests/RedirectTest.php +++ b/tests/RedirectTest.php @@ -8,9 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class RedirectTest extends PHPUnit\Framework\TestCase { private Engine $app; diff --git a/tests/RegisterTest.php b/tests/RegisterTest.php index dc383cd..bd67106 100644 --- a/tests/RegisterTest.php +++ b/tests/RegisterTest.php @@ -8,8 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; require_once __DIR__ . '/classes/User.php'; class RegisterTest extends PHPUnit\Framework\TestCase diff --git a/tests/RenderTest.php b/tests/RenderTest.php index 260caa6..dccdc69 100644 --- a/tests/RenderTest.php +++ b/tests/RenderTest.php @@ -8,9 +8,6 @@ use flight\Engine; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/Flight.php'; - class RenderTest extends PHPUnit\Framework\TestCase { private Engine $app; diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 842c1bc..aea65ee 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -7,9 +7,7 @@ */ use flight\net\Request; - -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; +use flight\util\Collection; class RequestTest extends PHPUnit\Framework\TestCase { @@ -17,6 +15,8 @@ class RequestTest extends PHPUnit\Framework\TestCase protected function setUp(): void { + $_SERVER = []; + $_REQUEST = []; $_SERVER['REQUEST_URI'] = '/'; $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -34,6 +34,11 @@ class RequestTest extends PHPUnit\Framework\TestCase $this->request = new Request(); } + protected function tearDown(): void { + unset($_REQUEST); + unset($_SERVER); + } + public function testDefaults() { self::assertEquals('/', $this->request->url); @@ -150,4 +155,42 @@ class RequestTest extends PHPUnit\Framework\TestCase $request = new Request(); 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()); + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php new file mode 100644 index 0000000..bbcdb62 --- /dev/null +++ b/tests/ResponseTest.php @@ -0,0 +1,227 @@ + + * @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()); + } + + +} diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 686d91f..9adc7fc 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -10,9 +10,6 @@ use flight\core\Dispatcher; use flight\net\Request; use flight\net\Router; -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class RouterTest extends PHPUnit\Framework\TestCase { private Router $router; @@ -23,11 +20,18 @@ class RouterTest extends PHPUnit\Framework\TestCase protected function setUp(): void { + $_SERVER = []; + $_REQUEST = []; $this->router = new Router(); $this->request = new Request(); $this->dispatcher = new Dispatcher(); } + protected function tearDown(): void { + unset($_REQUEST); + unset($_SERVER); + } + // Simple output public function ok() { @@ -101,6 +105,16 @@ class RouterTest extends PHPUnit\Framework\TestCase $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 public function testPostRoute() { @@ -293,4 +307,49 @@ class RouterTest extends PHPUnit\Framework\TestCase $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()); + } } diff --git a/tests/VariableTest.php b/tests/VariableTest.php index e7a610d..03f2900 100644 --- a/tests/VariableTest.php +++ b/tests/VariableTest.php @@ -5,9 +5,6 @@ * @copyright Copyright (c) 2012, Mike Cao * @license MIT, http://flightphp.com/license */ -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class VariableTest extends PHPUnit\Framework\TestCase { /** diff --git a/tests/ViewTest.php b/tests/ViewTest.php index bdfcfeb..2013861 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -5,9 +5,6 @@ * @copyright Copyright (c) 2012, Mike Cao * @license MIT, http://flightphp.com/license */ -require_once 'vendor/autoload.php'; -require_once __DIR__ . '/../flight/autoload.php'; - class ViewTest extends PHPUnit\Framework\TestCase { /** @@ -36,6 +33,21 @@ class ViewTest extends PHPUnit\Framework\TestCase $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 public function testTemplateExists() { @@ -51,6 +63,13 @@ class ViewTest extends PHPUnit\Framework\TestCase $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 public function testFetch() { @@ -79,4 +98,23 @@ class ViewTest extends PHPUnit\Framework\TestCase $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('<script>'); + $result = $this->view->e('