Commit version 24.12.13800

This commit is contained in:
2025-01-06 17:35:06 -05:00
parent b7f6a79c2c
commit 55d9218816
6133 changed files with 4239740 additions and 1374287 deletions

View File

@ -1,4 +0,0 @@
.idea
vendor/
composer.phar
composer.lock

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,918 +1,59 @@
[![Version](http://poser.pugx.org/flightphp/core/version)](https://packagist.org/packages/flightphp/core)
[![Monthly Downloads](http://poser.pugx.org/flightphp/core/d/monthly)](https://packagist.org/packages/flightphp/core)
![PHPStan: Level 6](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat)
[![License](http://poser.pugx.org/flightphp/core/license)](https://packagist.org/packages/flightphp/core)
[![PHP Version Require](http://poser.pugx.org/flightphp/core/require/php)](https://packagist.org/packages/flightphp/core)
![Matrix](https://img.shields.io/matrix/flight-php-framework%3Amatrix.org?server_fqdn=matrix.org&style=social&logo=matrix)
# 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.
```php
require 'flight/Flight.php';
# Basic Usage
Flight::route('/', function(){
echo 'hello world!';
```php
// 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();
```
[Learn more](http://flightphp.com/learn)
## Skeleton App
You can also install a skeleton app. Go to [flightphp/skeleton](https://github.com/flightphp/skeleton) for instructions on how to get started!
# Documentation
We have our own documentation website that is built with Flight (naturally). Learn more about the framework at [docs.flightphp.com](https://docs.flightphp.com).
# Community
Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/#/#flight-php-framework:matrix.org)
# Upgrading From v2
If you have a current project on v2, you should be able to upgrade to v2 with no issues depending on how your project was built. If there are any issues with upgrade, they are documented in the [migrating to v3](https://docs.flightphp.com/learn/migrating-to-v3) documentation page. It is the intention of Flight to maintain longterm stability of the project and to not add rewrites with major version changes.
# Requirements
Flight requires `PHP 5.3` or greater.
> [!IMPORTANT]
> Flight requires `PHP 7.4` or greater.
**Note:** PHP 7.4 is supported because at the current time of writing (2024) PHP 7.4 is the default version for some LTS Linux distributions. Forcing a move to PHP >8 would cause a lot of heartburn for those users.
The framework also supports PHP >8.
# Roadmap
To see the current and future roadmap for the Flight Framework, visit the [project roadmap](https://github.com/orgs/flightphp/projects/1/views/1)
# License
Flight is released under the [MIT](http://flightphp.com/license) license.
# Installation
1\. Download the files.
If you're using [Composer](https://getcomposer.org/), you can run the following command:
```
composer require mikecao/flight
```
OR you can [download](https://github.com/mikecao/flight/archive/master.zip) them directly
and extract them to your web directory.
2\. Configure your webserver.
For *Apache*, edit your `.htaccess` file with the following:
```
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
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`.
For *Nginx*, add the following to your server declaration:
```
server {
location / {
try_files $uri $uri/ /index.php;
}
}
```
3\. Create your `index.php` file.
First include the framework.
```php
require 'flight/Flight.php';
```
If you're using Composer, run the autoloader instead.
```php
require 'vendor/autoload.php';
```
Then define a route and assign a function to handle the request.
```php
Flight::route('/', function(){
echo 'hello world!';
});
```
Finally, start the framework.
```php
Flight::start();
```
# Routing
Routing in Flight is done by matching a URL pattern with a callback function.
```php
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!';
}
Flight::route('/', 'hello');
```
Or a class method:
```php
class Greeting {
public static function hello() {
echo 'hello world!';
}
}
Flight::route('/', array('Greeting', 'hello'));
```
Or an object method:
```php
class Greeting
{
public function __construct() {
$this->name = 'John Doe';
}
public function hello() {
echo "Hello, {$this->name}!";
}
}
$greeting = new Greeting();
Flight::route('/', array($greeting, 'hello'));
```
Routes are matched in the order they are defined. The first route to match a
request will be invoked.
## Method Routing
By default, route patterns are matched against all request methods. You can respond
to specific methods by placing an identifier before the URL.
```php
Flight::route('GET /', function(){
echo 'I received a GET 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.';
});
```
## Regular Expressions
You can use regular expressions in your routes:
```php
Flight::route('/user/[0-9]+', function(){
// This will match /user/1234
});
```
## Named Parameters
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)!";
});
```
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
});
```
## Optional Parameters
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){
// 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.
## Wildcards
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
});
```
To route all requests to a single callback, you can do:
```php
Flight::route('*', function(){
// Do something
});
```
## Passing
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/*', function(){
// This will get called
});
```
## Route Info
If you want to inspect the matching route information, you can request for the route
object to be passed to your callback by passing in `true` as the third parameter in
the route method. The route object will always be the last parameter passed to your
callback function.
```php
Flight::route('/', function($route){
// Array of HTTP methods matched against
$route->methods;
// Array of named parameters
$route->params;
// Matching regular expression
$route->regex;
// Contains the contents of any '*' used in the URL pattern
$route->splat;
}, true);
```
# Extending
Flight is designed to be an extensible framework. The framework comes with a set
of default methods and components, but it allows you to map your own methods,
register your own classes, or even override existing classes and methods.
## Mapping Methods
To map your own custom method, you use the `map` function:
```php
// Map your method
Flight::map('hello', function($name){
echo "hello $name!";
});
// Call your custom method
Flight::hello('Bob');
```
## Registering Classes
To register your own class, you use the `register` function:
```php
// Register your class
Flight::register('user', 'User');
// Get an instance of your class
$user = Flight::user();
```
The register method also allows you to pass along parameters to your class
constructor. So when you load your custom class, it will come pre-initialized.
You can define the constructor parameters by passing in an additional array.
Here's an example of loading a database connection:
```php
// Register class with constructor parameters
Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'));
// Get an instance of your class
// This will create an object with the defined parameters
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();
```
If you pass in an additional callback parameter, it will be executed immediately
after class construction. This allows you to perform any set up procedures for your
new object. The callback function takes one parameter, an instance of the new object.
```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);
});
```
By default, every time you load your class you will get a shared instance.
To get a new instance of a class, simply pass in `false` as a parameter:
```php
// Shared instance of the class
$shared = Flight::db();
// New instance of the class
$new = Flight::db(false);
```
Keep in mind that mapped methods have precedence over registered classes. If you
declare both using the same name, only the mapped method will be invoked.
# Overriding
Flight allows you to override its default functionality to suit your own needs,
without having to modify any code.
For example, when Flight cannot match a URL to a route, it invokes the `notFound`
method which sends a generic `HTTP 404` response. You can override this behavior
by using the `map` method:
```php
Flight::map('notFound', function(){
// Display custom 404 page
include 'errors/404.html';
});
```
Flight also allows you to replace core components of the framework.
For example you can replace the default Router class with your own custom class:
```php
// Register your custom class
Flight::register('router', 'MyRouter');
// When Flight loads the Router instance, it will load your class
$myrouter = Flight::router();
```
Framework methods like `map` and `register` however cannot be overridden. You will
get an error if you try to do so.
# Filtering
Flight allows you to filter methods before and after they are called. There are no
predefined hooks you need to memorize. You can filter any of the default framework
methods as well as any custom methods that you've mapped.
A filter function looks like this:
```php
function(&$params, &$output) {
// Filter code
}
```
Using the passed in variables you can manipulate the input parameters and/or the output.
You can have a filter run before a method by doing:
```php
Flight::before('start', function(&$params, &$output){
// Do something
});
```
You can have a filter run after a method by doing:
```php
Flight::after('start', function(&$params, &$output){
// Do something
});
```
You can add as many filters as you want to any method. They will be called in the
order that they are declared.
Here's an example of the filtering process:
```php
// Map a custom method
Flight::map('hello', function($name){
return "Hello, $name!";
});
// Add a before filter
Flight::before('hello', function(&$params, &$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!";
});
// Invoke the custom method
echo Flight::hello('Bob');
```
This should display:
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(&$params, &$output){
echo 'two';
// This will end the chain
return false;
});
// This will not get called
Flight::before('start', function(&$params, &$output){
echo 'three';
});
```
Note, core methods such as `map` and `register` cannot be filtered because they
are called directly and not invoked dynamically.
# Variables
Flight allows you to save variables so that they can be used anywhere in your application.
```php
// Save your variable
Flight::set('id', 123);
// Elsewhere in your application
$id = Flight::get('id');
```
To see if a variable has been set you can do:
```php
if (Flight::has('id')) {
// Do something
}
```
You can clear a variable by doing:
```php
// Clears the id variable
Flight::clear('id');
// Clears all variables
Flight::clear();
```
Flight also uses variables for configuration purposes.
```php
Flight::set('flight.log_errors', true);
```
# Views
Flight provides some basic templating functionality by default. To display a view
template call the `render` method with the name of the template file and optional
template data:
```php
Flight::render('hello.php', array('name' => 'Bob'));
```
The template data you pass in is automatically injected into the template and can
be reference like a local variable. Template files are simply PHP files. If the
content of the `hello.php` template file is:
```php
Hello, '<?php echo $name; ?>'!
```
The output would be:
Hello, Bob!
You can also manually set view variables by using the set method:
```php
Flight::view()->set('name', 'Bob');
```
The variable `name` is now available across all your views. So you can simply do:
```php
Flight::render('hello');
```
Note that when specifying the name of the template in the render method, you can
leave out the `.php` extension.
By default Flight will look for a `views` directory for template files. You can
set an alternate path for your templates by setting the following config:
```php
Flight::set('flight.views.path', '/path/to/views');
```
## Layouts
It is common for websites to have a single layout template file with interchanging
content. To render content to be used in a layout, you can pass in an optional
parameter to the `render` method.
```php
Flight::render('header', array('heading' => 'Hello'), 'header_content');
Flight::render('body', array('body' => 'World'), 'body_content');
```
Your view will then have saved variables called `header_content` and `body_content`.
You can then render your layout by doing:
```php
Flight::render('layout', array('title' => 'Home Page'));
```
If the template files looks like this:
`header.php`:
```php
<h1><?php echo $heading; ?></h1>
```
`body.php`:
```php
<div><?php echo $body; ?></div>
```
`layout.php`:
```php
<html>
<head>
<title><?php echo $title; ?></title>
</head>
<body>
<?php echo $header_content; ?>
<?php echo $body_content; ?>
</body>
</html>
```
The output would be:
```html
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Hello</h1>
<div>World</div>
</body>
</html>
```
## Custom Views
Flight allows you to swap out the default view engine simply by registering your
own view class. Here's how you would use the [Smarty](http://www.smarty.net/)
template engine for your views:
```php
// Load Smarty library
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/';
});
// Assign template data
Flight::view()->assign('name', 'Bob');
// Display the template
Flight::view()->display('hello.tpl');
```
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);
});
```
# Error Handling
## Errors and Exceptions
All errors and exceptions are caught by Flight and passed to the `error` method.
The default behavior is to send a generic `HTTP 500 Internal Server Error`
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();
});
```
By default errors are not logged to the web server. You can enable this by
changing the config:
```php
Flight::set('flight.log_errors', true);
```
## Not Found
When a URL can't be found, Flight calls the `notFound` method. The default
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
});
```
# Redirects
You can redirect the current request by using the `redirect` method and passing
in a new URL:
```php
Flight::redirect('/new/location');
```
By default Flight sends a HTTP 303 status code. You can optionally set a
custom code:
```php
Flight::redirect('/new/location', 401);
```
# Requests
Flight encapsulates the HTTP request into a single object, which can be
accessed by doing:
```php
$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
```
You can access the `query`, `data`, `cookies`, and `files` properties
as arrays or objects.
So, to get a query string parameter, you can do:
```php
$id = Flight::request()->query['id'];
```
Or you can do:
```php
$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:
```php
$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:
```php
$id = Flight::request()->data->id;
```
# HTTP Caching
Flight provides built-in support for HTTP level caching. If the caching condition
is met, Flight will return an HTTP `304 Not Modified` response. The next time the
client requests the same resource, they will be prompted to use their locally
cached version.
## Last-Modified
You can use the `lastModified` method and pass in a UNIX timestamp to set the date
and time a page was last modified. The client will continue to use their cache until
the last modified value is changed.
```php
Flight::route('/news', function(){
Flight::lastModified(1234567890);
echo 'This content will be cached.';
});
```
## ETag
`ETag` caching is similar to `Last-Modified`, except you can specify any id you
want for the resource:
```php
Flight::route('/news', function(){
Flight::etag('my-unique-id');
echo 'This content will be cached.';
});
```
Keep in mind that calling either `lastModified` or `etag` will both set and check the
cache value. If the cache value is the same between requests, Flight will immediately
send an `HTTP 304` response and stop processing.
# Stopping
You can stop the framework at any point by calling the `halt` method:
```php
Flight::halt();
```
You can also specify an optional `HTTP` status code and message:
```php
Flight::halt(200, 'Be right back...');
```
Calling `halt` will discard any response content up to that point. If you want to stop
the framework and output the current response, use the `stop` method:
```php
Flight::stop();
```
# JSON
Flight provides support for sending JSON and JSONP responses. To send a JSON response you
pass some data to be JSON encoded:
```php
Flight::json(array('id' => 123));
```
For JSONP requests you, can optionally pass in the query parameter name you are
using to define your callback function:
```php
Flight::jsonp(array('id' => 123), 'q');
```
So, when making a GET request using `?q=my_func`, you should receive the output:
```
my_func({"id":123});
```
If you don't pass in a query parameter name it will default to `jsonp`.
# Configuration
You can customize certain behaviors of Flight by setting configuration values
through the `set` method.
```php
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)
# Framework Methods
Flight is designed to be easy to use and understand. The following is the complete
set of methods for the framework. It consists of core methods, which are regular
static methods, and extensible methods, which are mapped methods that can be filtered
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::init() // Initializes the framework to its default settings.
Flight::app() // Gets the application object instance
```
## Extensible Methods
```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::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.
```
Any custom methods added with `map` and `register` can also be filtered.
# Framework Instance
Instead of running Flight as a global static class, you can optionally run it
as an object instance.
```php
require 'flight/autoload.php';
use flight\Engine;
$app = new Engine();
$app->route('/', function(){
echo 'hello world!';
});
$app->start();
```
So instead of calling the static method, you would call the instance method with
the same name on the Engine object.
Flight is released under the [MIT](http://docs.flightphp.com/license) license.

View File

@ -1 +1 @@
1.3.9
3.13.0

View File

@ -1,11 +1,10 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2013, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
require_once __DIR__.'/core/Loader.php';
declare(strict_types=1);
\flight\core\Loader::autoload(true, dirname(__DIR__));
use flight\core\Loader;
require_once __DIR__ . '/Flight.php';
require_once __DIR__ . '/core/Loader.php';
Loader::autoload(true, [dirname(__DIR__)]);

View File

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

View File

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

View File

@ -1,56 +1,150 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
declare(strict_types=1);
namespace flight\core;
use Exception;
use flight\Engine;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use ReflectionFunction;
use Throwable;
use TypeError;
/**
* The Dispatcher class is responsible for dispatching events. Events
* are simply aliases for class methods or functions. The Dispatcher
* allows you to hook other functions to an event that can modify the
* input parameters and/or the output.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Dispatcher {
/**
* Mapped events.
*
* @var array
*/
protected $events = array();
class Dispatcher
{
public const FILTER_BEFORE = 'before';
public const FILTER_AFTER = 'after';
/** Exception message if thrown by setting the container as a callable method. */
protected ?Throwable $containerException = null;
/** @var ?Engine $engine Engine instance. */
protected ?Engine $engine = null;
/** @var array<string, callable(): (void|mixed)> Mapped events. */
protected array $events = [];
/**
* Method filters.
*
* @var array
* @var array<string, array<'before'|'after', array<int, callable(array<int, mixed> &$params, mixed &$output): (void|false)>>>
*/
protected $filters = array();
protected array $filters = [];
/**
* This is a container for the dependency injection.
*
* @var null|ContainerInterface|(callable(string $classString, array<int, mixed> $params): (null|object))
*/
protected $containerHandler = null;
/**
* Sets the dependency injection container handler.
*
* @param ContainerInterface|(callable(string $classString, array<int, mixed> $params): (null|object)) $containerHandler
* Dependency injection container.
*
* @throws InvalidArgumentException If $containerHandler is not a `callable` or instance of `Psr\Container\ContainerInterface`.
*/
public function setContainerHandler($containerHandler): void
{
$containerInterfaceNS = '\Psr\Container\ContainerInterface';
if (
is_a($containerHandler, $containerInterfaceNS)
|| is_callable($containerHandler)
) {
$this->containerHandler = $containerHandler;
return;
}
throw new InvalidArgumentException(
"\$containerHandler must be of type callable or instance $containerInterfaceNS"
);
}
public function setEngine(Engine $engine): void
{
$this->engine = $engine;
}
/**
* Dispatches an event.
*
* @param string $name Event name
* @param array $params Callback parameters
* @return string Output of callback
* @throws \Exception
* @param string $name Event name.
* @param array<int, mixed> $params Callback parameters.
*
* @return mixed Output of callback
* @throws Exception If event name isn't found or if event throws an `Exception`.
*/
public function run($name, array $params = array()) {
$output = '';
public function run(string $name, array $params = [])
{
$this->runPreFilters($name, $params);
$output = $this->runEvent($name, $params);
// Run pre-filters
if (!empty($this->filters[$name]['before'])) {
$this->filter($this->filters[$name]['before'], $params, $output);
return $this->runPostFilters($name, $output);
}
/**
* @param array<int, mixed> &$params
*
* @return $this
* @throws Exception
*/
protected function runPreFilters(string $eventName, array &$params): self
{
$thereAreBeforeFilters = !empty($this->filters[$eventName][self::FILTER_BEFORE]);
if ($thereAreBeforeFilters) {
$this->filter($this->filters[$eventName][self::FILTER_BEFORE], $params, $output);
}
// Run requested method
$output = $this->execute($this->get($name), $params);
return $this;
}
// Run post-filters
if (!empty($this->filters[$name]['after'])) {
$this->filter($this->filters[$name]['after'], $params, $output);
/**
* @param array<int, mixed> &$params
*
* @return void|mixed
* @throws Exception
*/
protected function runEvent(string $eventName, array &$params)
{
$requestedMethod = $this->get($eventName);
if ($requestedMethod === null) {
throw new Exception("Event '$eventName' isn't found.");
}
return $this->execute($requestedMethod, $params);
}
/**
* @param mixed &$output
*
* @return mixed
* @throws Exception
*/
protected function runPostFilters(string $eventName, &$output)
{
static $params = [];
$thereAreAfterFilters = !empty($this->filters[$eventName][self::FILTER_AFTER]);
if ($thereAreAfterFilters) {
$this->filter($this->filters[$eventName][self::FILTER_AFTER], $params, $output);
}
return $output;
@ -59,174 +153,352 @@ class Dispatcher {
/**
* Assigns a callback to an event.
*
* @param string $name Event name
* @param callback $callback Callback function
* @param string $name Event name.
* @param callable(): (void|mixed) $callback Callback function.
*
* @return $this
*/
public function set($name, $callback) {
public function set(string $name, callable $callback): self
{
$this->events[$name] = $callback;
return $this;
}
/**
* Gets an assigned callback.
*
* @param string $name Event name
* @return callback $callback Callback function
* @param string $name Event name.
*
* @return null|(callable(): (void|mixed)) $callback Callback function.
*/
public function get($name) {
return isset($this->events[$name]) ? $this->events[$name] : null;
public function get(string $name): ?callable
{
return $this->events[$name] ?? null;
}
/**
* Checks if an event has been set.
*
* @param string $name Event name
* @return bool Event status
* @param string $name Event name.
*
* @return bool If event exists or doesn't exists.
*/
public function has($name) {
public function has(string $name): bool
{
return isset($this->events[$name]);
}
/**
* Clears an event. If no name is given,
* all events are removed.
* Clears an event. If no name is given, all events will be removed.
*
* @param string $name Event name
* @param ?string $name Event name.
*/
public function clear($name = null) {
public function clear(?string $name = null): void
{
if ($name !== null) {
unset($this->events[$name]);
unset($this->filters[$name]);
return;
}
else {
$this->events = array();
$this->filters = array();
}
$this->reset();
}
/**
* Hooks a callback to an event.
*
* @param string $name Event name
* @param string $type Filter type
* @param callback $callback Callback function
* @param 'before'|'after' $type Filter type.
* @param callable(array<int, mixed> &$params, mixed &$output): (void|false)|callable(mixed &$output): (void|false) $callback
*
* @return $this
*/
public function hook($name, $type, $callback) {
public function hook(string $name, string $type, callable $callback): self
{
static $filterTypes = [self::FILTER_BEFORE, self::FILTER_AFTER];
if (!in_array($type, $filterTypes, true)) {
$noticeMessage = "Invalid filter type '$type', use " . join('|', $filterTypes);
trigger_error($noticeMessage, E_USER_NOTICE);
}
if ($type === self::FILTER_AFTER) {
$callbackInfo = new ReflectionFunction($callback);
$parametersNumber = $callbackInfo->getNumberOfParameters();
if ($parametersNumber === 1) {
/** @disregard &$params in after filters are deprecated. */
$callback = fn (array &$params, &$output) => $callback($output);
}
}
$this->filters[$name][$type][] = $callback;
return $this;
}
/**
* Executes a chain of method filters.
*
* @param array $filters Chain of filters
* @param array $params Method parameters
* @param mixed $output Method output
* @throws \Exception
* @param array<int, callable(array<int, mixed> &$params, mixed &$output): (void|false)> $filters
* Chain of filters.
* @param array<int, mixed> $params Method parameters.
* @param mixed $output Method output.
*
* @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter.
*/
public function filter($filters, &$params, &$output) {
$args = array(&$params, &$output);
foreach ($filters as $callback) {
$continue = $this->execute($callback, $args);
if ($continue === false) break;
public function filter(array $filters, array &$params, &$output): void
{
foreach ($filters as $key => $callback) {
if (!is_callable($callback)) {
throw new InvalidArgumentException("Invalid callable \$filters[$key].");
}
$continue = $callback($params, $output);
if ($continue === false) {
break;
}
}
}
/**
* Executes a callback function.
*
* @param callback $callback Callback function
* @param array $params Function parameters
* @return mixed Function results
* @throws \Exception
* @param callable-string|(callable(): mixed)|array{class-string|object, string} $callback
* Callback function.
* @param array<int, mixed> $params Function parameters.
*
* @return mixed Function results.
* @throws Exception If `$callback` also throws an `Exception`.
*/
public static function execute($callback, array &$params = array()) {
if (is_callable($callback)) {
return is_array($callback) ?
self::invokeMethod($callback, $params) :
self::callFunction($callback, $params);
public function execute($callback, array &$params = [])
{
if (
is_string($callback) === true
&& (strpos($callback, '->') !== false || strpos($callback, '::') !== false)
) {
$callback = $this->parseStringClassAndMethod($callback);
}
else {
throw new \Exception('Invalid callback specified.');
return $this->invokeCallable($callback, $params);
}
/**
* Parses a string into a class and method.
*
* @param string $classAndMethod Class and method
*
* @return array{0: class-string|object, 1: string} Class and method
*/
public function parseStringClassAndMethod(string $classAndMethod): array
{
$classParts = explode('->', $classAndMethod);
if (count($classParts) === 1) {
$classParts = explode('::', $classParts[0]);
}
return $classParts;
}
/**
* Calls a function.
*
* @param string $func Name of function to call
* @param array $params Function parameters
* @return mixed Function results
* @param callable $func Name of function to call.
* @param array<int, mixed> &$params Function parameters.
*
* @return mixed Function results.
* @deprecated 3.7.0 Use invokeCallable instead
*/
public static function callFunction($func, array &$params = array()) {
// Call static method
if (is_string($func) && strpos($func, '::') !== false) {
return call_user_func_array($func, $params);
}
switch (count($params)) {
case 0:
return $func();
case 1:
return $func($params[0]);
case 2:
return $func($params[0], $params[1]);
case 3:
return $func($params[0], $params[1], $params[2]);
case 4:
return $func($params[0], $params[1], $params[2], $params[3]);
case 5:
return $func($params[0], $params[1], $params[2], $params[3], $params[4]);
default:
return call_user_func_array($func, $params);
}
public function callFunction(callable $func, array &$params = [])
{
return $this->invokeCallable($func, $params);
}
/**
* Invokes a method.
*
* @param mixed $func Class method
* @param array $params Class method parameters
* @return mixed Function results
* @param array{0: class-string|object, 1: string} $func Class method.
* @param array<int, mixed> &$params Class method parameters.
*
* @return mixed Function results.
* @throws TypeError For nonexistent class name.
* @deprecated 3.7.0 Use invokeCallable instead.
*/
public static function invokeMethod($func, array &$params = array()) {
list($class, $method) = $func;
public function invokeMethod(array $func, array &$params = [])
{
return $this->invokeCallable($func, $params);
}
$instance = is_object($class);
switch (count($params)) {
case 0:
return ($instance) ?
$class->$method() :
$class::$method();
case 1:
return ($instance) ?
$class->$method($params[0]) :
$class::$method($params[0]);
case 2:
return ($instance) ?
$class->$method($params[0], $params[1]) :
$class::$method($params[0], $params[1]);
case 3:
return ($instance) ?
$class->$method($params[0], $params[1], $params[2]) :
$class::$method($params[0], $params[1], $params[2]);
case 4:
return ($instance) ?
$class->$method($params[0], $params[1], $params[2], $params[3]) :
$class::$method($params[0], $params[1], $params[2], $params[3]);
case 5:
return ($instance) ?
$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:
return call_user_func_array($func, $params);
/**
* Invokes a callable (anonymous function or Class->method).
*
* @param array{0: class-string|object, 1: string}|callable $func Class method.
* @param array<int, mixed> &$params Class method parameters.
*
* @return mixed Function results.
* @throws TypeError For nonexistent class name.
* @throws InvalidArgumentException If the constructor requires parameters.
* @version 3.7.0
*/
public function invokeCallable($func, array &$params = [])
{
// If this is a directly callable function, call it
if (is_array($func) === false) {
$this->verifyValidFunction($func);
return call_user_func_array($func, $params);
}
[$class, $method] = $func;
$mustUseTheContainer = $this->mustUseContainer($class);
if ($mustUseTheContainer === true) {
$resolvedClass = $this->resolveContainerClass($class, $params);
if ($resolvedClass) {
$class = $resolvedClass;
}
}
$this->verifyValidClassCallable($class, $method, $resolvedClass ?? null);
// Class is a string, and method exists, create the object by hand and inject only the Engine
if (is_string($class)) {
$class = new $class($this->engine);
}
return call_user_func_array([$class, $method], $params);
}
/**
* Handles invalid callback types.
*
* @param callable-string|(callable(): mixed)|array{0: class-string|object, 1: string} $callback
* Callback function.
*
* @throws InvalidArgumentException If `$callback` is an invalid type.
*/
protected function verifyValidFunction($callback): void
{
if (is_string($callback) && !function_exists($callback)) {
throw new InvalidArgumentException('Invalid callback specified.');
}
}
/**
* Verifies if the provided class and method are valid callable.
*
* @param class-string|object $class The class name.
* @param string $method The method name.
* @param object|null $resolvedClass The resolved class.
*
* @throws Exception If the class or method is not found.
*/
protected function verifyValidClassCallable($class, $method, $resolvedClass): void
{
$exception = null;
// Final check to make sure it's actually a class and a method, or throw an error
if (is_object($class) === false && class_exists($class) === false) {
$exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?");
// If this tried to resolve a class in a container and failed somehow, throw the exception
} elseif (!$resolvedClass && $this->containerException !== null) {
$exception = $this->containerException;
// Class is there, but no method
} elseif (is_object($class) === true && method_exists($class, $method) === false) {
$classNamespace = get_class($class);
$exception = new Exception("Class found, but method '$classNamespace::$method' not found.");
}
if ($exception !== null) {
$this->fixOutputBuffering();
throw $exception;
}
}
/**
* Resolves the container class.
*
* @param class-string $class Class name.
* @param array<int, mixed> &$params Class constructor parameters.
*
* @return ?object Class object.
*/
public function resolveContainerClass(string $class, array &$params)
{
// PSR-11
if (
is_a($this->containerHandler, '\Psr\Container\ContainerInterface')
&& $this->containerHandler->has($class)
) {
return $this->containerHandler->get($class);
}
// Just a callable where you configure the behavior (Dice, PHP-DI, etc.)
if (is_callable($this->containerHandler)) {
/* This is to catch all the error that could be thrown by whatever
container you are using */
try {
return ($this->containerHandler)($class, $params);
// could not resolve a class for some reason
} catch (Exception $exception) {
// If the container throws an exception, we need to catch it
// and store it somewhere. If we just let it throw itself, it
// doesn't properly close the output buffers and can cause other
// issues.
// This is thrown in the verifyValidClassCallable method.
$this->containerException = $exception;
}
}
return null;
}
/**
* Checks to see if a container should be used or not.
*
* @param string|object $class the class to verify
*
* @return boolean
*/
public function mustUseContainer($class): bool
{
return $this->containerHandler !== null && (
(is_object($class) === true && strpos(get_class($class), 'flight\\') === false)
|| is_string($class)
);
}
/** Because this could throw an exception in the middle of an output buffer, */
protected function fixOutputBuffering(): void
{
// Cause PHPUnit has 1 level of output buffering by default
if (ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) {
ob_end_clean();
}
}
/**
* Resets the object to the initial state.
*
* @return $this
*/
public function reset() {
$this->events = array();
$this->filters = array();
public function reset(): self
{
$this->events = [];
$this->filters = [];
return $this;
}
}

View File

@ -1,53 +1,64 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
declare(strict_types=1);
namespace flight\core;
use Closure;
use Exception;
/**
* The Loader class is responsible for loading objects. It maintains
* a list of reusable class instances and can generate a new class
* instances with custom initialization parameters. It also performs
* class autoloading.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Loader {
class Loader
{
/**
* Registered classes.
*
* @var array
* @var array<string, array{class-string|Closure(): object, array<int, mixed>, ?callable}> $classes
*/
protected $classes = array();
protected array $classes = [];
/**
* If this is disabled, classes can load with underscores
*/
protected static bool $v2ClassLoading = true;
/**
* Class instances.
*
* @var array
* @var array<string, object>
*/
protected $instances = array();
protected array $instances = [];
/**
* Autoload directories.
*
* @var array
* @var array<int, string>
*/
protected static $dirs = array();
protected static array $dirs = [];
/**
* Registers a class.
*
* @param string $name Registry name
* @param string|callable $class Class name or function to instantiate class
* @param array $params Class initialization parameters
* @param callback $callback Function to call after object instantiation
* @param class-string<T>|Closure(): T $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param ?Closure(T $instance): void $callback $callback Function to call after object instantiation
*
* @template T of object
*/
public function register($name, $class, array $params = array(), $callback = null) {
public function register(string $name, $class, array $params = [], ?callable $callback = null): void
{
unset($this->instances[$name]);
$this->classes[$name] = array($class, $params, $callback);
$this->classes[$name] = [$class, $params, $callback];
}
/**
@ -55,23 +66,27 @@ class Loader {
*
* @param string $name Registry name
*/
public function unregister($name) {
public function unregister(string $name): void
{
unset($this->classes[$name]);
}
/**
* Loads a registered class.
*
* @param string $name Method name
* @param bool $shared Shared instance
* @return object Class instance
* @throws \Exception
* @param string $name Method name
* @param bool $shared Shared instance
*
* @throws Exception
*
* @return ?object Class instance
*/
public function load($name, $shared = true) {
public function load(string $name, bool $shared = true): ?object
{
$obj = null;
if (isset($this->classes[$name])) {
list($class, $params, $callback) = $this->classes[$name];
[0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name];
$exists = isset($this->instances[$name]);
@ -79,18 +94,17 @@ class Loader {
$obj = ($exists) ?
$this->getInstance($name) :
$this->newInstance($class, $params);
if (!$exists) {
$this->instances[$name] = $obj;
}
}
else {
} else {
$obj = $this->newInstance($class, $params);
}
if ($callback && (!$shared || !$exists)) {
$ref = array(&$obj);
call_user_func_array($callback, $ref);
$ref = [&$obj];
\call_user_func_array($callback, $ref);
}
}
@ -101,78 +115,70 @@ class Loader {
* Gets a single instance of a class.
*
* @param string $name Instance name
* @return object Class instance
*
* @return ?object Class instance
*/
public function getInstance($name) {
return isset($this->instances[$name]) ? $this->instances[$name] : null;
public function getInstance(string $name): ?object
{
return $this->instances[$name] ?? null;
}
/**
* Gets a new instance of a class.
*
* @param string|callable $class Class name or callback function to instantiate class
* @param array $params Class initialization parameters
* @return object Class instance
* @throws \Exception
* @param class-string<T>|Closure(): class-string<T> $class Class name or callback function to instantiate class
* @param array<int, string> $params Class initialization parameters
*
* @template T of object
*
* @throws Exception
*
* @return T Class instance
*/
public function newInstance($class, array $params = array()) {
if (is_callable($class)) {
return call_user_func_array($class, $params);
public function newInstance($class, array $params = [])
{
if (\is_callable($class)) {
return \call_user_func_array($class, $params);
}
switch (count($params)) {
case 0:
return new $class();
case 1:
return new $class($params[0]);
case 2:
return new $class($params[0], $params[1]);
case 3:
return new $class($params[0], $params[1], $params[2]);
case 4:
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]);
default:
try {
$refClass = new \ReflectionClass($class);
return $refClass->newInstanceArgs($params);
} catch (\ReflectionException $e) {
throw new \Exception("Cannot instantiate {$class}", 0, $e);
}
}
return new $class(...$params);
}
/**
* Gets a registered callable
*
* @param string $name Registry name
*
* @return mixed Class information or null if not registered
*/
public function get($name) {
return isset($this->classes[$name]) ? $this->classes[$name] : null;
public function get(string $name)
{
return $this->classes[$name] ?? null;
}
/**
* Resets the object to the initial state.
*/
public function reset() {
$this->classes = array();
$this->instances = array();
public function reset(): void
{
$this->classes = [];
$this->instances = [];
}
/*** Autoloading Functions ***/
// Autoloading Functions
/**
* Starts/stops autoloader.
*
* @param bool $enabled Enable/disable autoloading
* @param array $dirs Autoload directories
* @param bool $enabled Enable/disable autoloading
* @param string|iterable<int, string> $dirs Autoload directories
*/
public static function autoload($enabled = true, $dirs = array()) {
public static function autoload(bool $enabled = true, $dirs = []): void
{
if ($enabled) {
spl_autoload_register(array(__CLASS__, 'loadClass'));
}
else {
spl_autoload_unregister(array(__CLASS__, 'loadClass'));
spl_autoload_register([__CLASS__, 'loadClass']);
} else {
spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore
}
if (!empty($dirs)) {
@ -183,15 +189,20 @@ class Loader {
/**
* Autoloads classes.
*
* Classes are not allowed to have underscores in their names.
*
* @param string $class Class name
*/
public static function loadClass($class) {
$class_file = str_replace(array('\\', '_'), '/', $class).'.php';
public static function loadClass(string $class): void
{
$replace_chars = self::$v2ClassLoading === true ? ['\\', '_'] : ['\\'];
$classFile = str_replace($replace_chars, '/', $class) . '.php';
foreach (self::$dirs as $dir) {
$file = $dir.'/'.$class_file;
if (file_exists($file)) {
require $file;
$filePath = "$dir/$classFile";
if (file_exists($filePath)) {
require_once $filePath;
return;
}
}
@ -200,16 +211,31 @@ class Loader {
/**
* Adds a directory for autoloading classes.
*
* @param mixed $dir Directory path
* @param string|iterable<int, string> $dir Directory path
*/
public static function addDirectory($dir) {
if (is_array($dir) || is_object($dir)) {
public static function addDirectory($dir): void
{
if (\is_array($dir) || \is_object($dir)) {
foreach ($dir as $value) {
self::addDirectory($value);
}
}
else if (is_string($dir)) {
if (!in_array($dir, self::$dirs)) self::$dirs[] = $dir;
} elseif (\is_string($dir)) {
if (!\in_array($dir, self::$dirs, true)) {
self::$dirs[] = $dir;
}
}
}
/**
* Sets the value for V2 class loading.
*
* @param bool $value The value to set for V2 class loading.
*
* @return void
*/
public static function setV2ClassLoading(bool $value): void
{
self::$v2ClassLoading = $value;
}
}

View File

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

View File

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

View File

@ -1,50 +1,41 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
declare(strict_types=1);
namespace flight\net;
use Exception;
/**
* The Response class represents an HTTP response. The object
* contains the response headers, HTTP status code, and response
* body.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Response {
class Response
{
/**
* @var int HTTP status
* Content-Length header.
*/
protected $status = 200;
public bool $content_length = true;
/**
* @var array HTTP headers
*/
protected $headers = array();
/**
* @var string HTTP response body
*/
protected $body;
/**
* @var bool HTTP response sent
*/
protected $sent = false;
/**
* header Content-Length
* This is to maintain legacy handling of output buffering
* which causes a lot of problems. This will be removed
* in v4
*
* @var boolean
*/
public $content_length = true;
public bool $v2_output_buffering = false;
/**
* @var array HTTP status codes
* HTTP status codes
*
* @var array<int, ?string> $codes
*/
public static $codes = array(
public static array $codes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
@ -112,26 +103,57 @@ class Response {
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required'
);
511 => 'Network Authentication Required',
];
/**
* HTTP status
*/
protected int $status = 200;
/**
* HTTP response headers
*
* @var array<string,int|string|array<int,string>> $headers
*/
protected array $headers = [];
/**
* HTTP response body
*/
protected string $body = '';
/**
* HTTP response sent
*/
protected bool $sent = false;
/**
* These are callbacks that can process the response body before it's sent
*
* @var array<int, callable> $responseBodyCallbacks
*/
protected array $responseBodyCallbacks = [];
/**
* Sets the HTTP status of the response.
*
* @param int $code HTTP status code.
* @return object|int Self reference
* @throws \Exception If invalid status code
* @param ?int $code HTTP status code.
*
* @throws Exception If invalid status code
*
* @return int|$this Self reference
*/
public function status($code = null) {
public function status(?int $code = null)
{
if ($code === null) {
return $this->status;
}
if (array_key_exists($code, self::$codes)) {
if (\array_key_exists($code, self::$codes)) {
$this->status = $code;
}
else {
throw new \Exception('Invalid status code.');
} else {
throw new Exception('Invalid status code.');
}
return $this;
@ -140,17 +162,18 @@ class Response {
/**
* Adds a header to the response.
*
* @param string|array $name Header name or array of names and values
* @param string $value Header value
* @return object Self reference
* @param array<string, int|string>|string $name Header name or array of names and values
* @param ?string $value Header value
*
* @return $this
*/
public function header($name, $value = null) {
if (is_array($name)) {
public function header($name, ?string $value = null): self
{
if (\is_array($name)) {
foreach ($name as $k => $v) {
$this->headers[$k] = $v;
}
}
else {
} else {
$this->headers[$name] = $value;
}
@ -158,34 +181,98 @@ class Response {
}
/**
* Returns the headers from the response
* @return array
* Gets a single header from the response.
*
* @param string $name the name of the header
*
* @return string|null
*/
public function headers() {
public function getHeader(string $name): ?string
{
$headers = $this->headers;
// lowercase all the header keys
$headers = array_change_key_case($headers, CASE_LOWER);
return $headers[strtolower($name)] ?? null;
}
/**
* Alias of Response->header(). Adds a header to the response.
*
* @param array<string, int|string>|string $name Header name or array of names and values
* @param ?string $value Header value
*
* @return $this
*/
public function setHeader($name, ?string $value): self
{
return $this->header($name, $value);
}
/**
* Returns the headers from the response.
*
* @return array<string, int|string|array<int, string>>
*/
public function headers(): array
{
return $this->headers;
}
/**
* Alias for Response->headers(). Returns the headers from the response.
*
* @return array<string, int|string|array<int, string>>
*/
public function getHeaders(): array
{
return $this->headers();
}
/**
* Writes content to the response body.
*
* @param string $str Response content
* @return object Self reference
* @param bool $overwrite Overwrite the response body
*
* @return $this Self reference
*/
public function write($str) {
public function write(string $str, bool $overwrite = false): self
{
if ($overwrite === true) {
$this->clearBody();
}
$this->body .= $str;
return $this;
}
/**
* Clears the response body.
*
* @return $this Self reference
*/
public function clearBody(): self
{
$this->body = '';
return $this;
}
/**
* Clears the response.
*
* @return object Self reference
* @return $this Self reference
*/
public function clear() {
public function clear(): self
{
$this->status = 200;
$this->headers = array();
$this->body = '';
$this->headers = [];
$this->clearBody();
// This needs to clear the output buffer if it's on
if ($this->v2_output_buffering === false && ob_get_length() > 0) {
ob_clean();
}
return $this;
}
@ -193,39 +280,40 @@ class Response {
/**
* Sets caching headers for the response.
*
* @param int|string $expires Expiration time
* @return object Self reference
* @param int|string|false $expires Expiration time as time() or as strtotime() string value
*
* @return $this Self reference
*/
public function cache($expires) {
if ($expires === false) {
public function cache($expires): self
{
if ($expires === false || $expires === 0) {
$this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
$this->headers['Cache-Control'] = array(
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0'
);
$this->headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0';
$this->headers['Pragma'] = 'no-cache';
}
else {
$expires = is_int($expires) ? $expires : strtotime($expires);
} else {
$expires = \is_int($expires) ? $expires : strtotime($expires);
$this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT';
$this->headers['Cache-Control'] = 'max-age='.($expires - time());
if (isset($this->headers['Pragma']) && $this->headers['Pragma'] == 'no-cache'){
$this->headers['Cache-Control'] = 'max-age=' . ($expires - time());
if (isset($this->headers['Pragma']) && $this->headers['Pragma'] === 'no-cache') {
unset($this->headers['Pragma']);
}
}
return $this;
}
/**
* Sends HTTP headers.
*
* @return object Self reference
* @return $this Self reference
*/
public function sendHeaders() {
public function sendHeaders(): self
{
// Send status code header
if (strpos(php_sapi_name(), 'cgi') !== false) {
header(
if (strpos(\PHP_SAPI, 'cgi') !== false) {
// @codeCoverageIgnoreStart
$this->setRealHeader(
sprintf(
'Status: %d %s',
$this->status,
@ -233,37 +321,37 @@ class Response {
),
true
);
}
else {
header(
// @codeCoverageIgnoreEnd
} else {
$this->setRealHeader(
sprintf(
'%s %d %s',
(isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'),
$_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
$this->status,
self::$codes[$this->status]),
self::$codes[$this->status]
),
true,
$this->status
);
}
// Send other headers
foreach ($this->headers as $field => $value) {
if (is_array($value)) {
foreach ($value as $v) {
header($field.': '.$v, false);
}
}
else {
header($field.': '.$value);
}
}
if ($this->content_length) {
if ($this->content_length === true) {
// Send content length
$length = $this->getContentLength();
if ($length > 0) {
header('Content-Length: '.$length);
$this->setHeader('Content-Length', (string) $length);
}
}
// Send other headers
foreach ($this->headers as $field => $value) {
if (\is_array($value)) {
foreach ($value as $v) {
$this->setRealHeader($field . ': ' . $v, false);
}
} else {
$this->setRealHeader($field . ': ' . $value);
}
}
@ -271,32 +359,84 @@ class Response {
}
/**
* Gets the content length.
* Sets a real header. Mostly used for test mocking.
*
* @return string Content length
* @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 getContentLength() {
return extension_loaded('mbstring') ?
mb_strlen($this->body, 'latin1') :
strlen($this->body);
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self
{
header($header_string, $replace, $response_code);
return $this;
}
/**
* Gets whether response was sent.
* Gets the content length.
*/
public function sent() {
public function getContentLength(): int
{
return \extension_loaded('mbstring') ?
mb_strlen($this->body, 'latin1') :
\strlen($this->body);
}
/**
* Gets the response body
*
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* Gets whether response body was sent.
*/
public function sent(): bool
{
return $this->sent;
}
/**
* Marks the response as sent.
*/
public function markAsSent(): void
{
$this->sent = true;
}
/**
* Sends a HTTP response.
*/
public function send() {
if (ob_get_length() > 0) {
ob_end_clean();
public function send(): void
{
// legacy way of handling this
if ($this->v2_output_buffering === true) {
if (ob_get_length() > 0) {
ob_end_clean(); // @codeCoverageIgnore
}
}
if (!headers_sent()) {
// Only for the v3 output buffering.
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
}
if ($this->headersSent() === false) {
// If you haven't set a Cache-Control header, we'll assume you don't want caching
if ($this->getHeader('Cache-Control') === null) {
$this->cache(false);
}
$this->sendHeaders();
}
@ -304,5 +444,78 @@ class Response {
$this->sent = true;
}
}
/**
* Headers have been sent
*
* @return bool
* @codeCoverageIgnore
*/
public function headersSent(): bool
{
return headers_sent();
}
/**
* Adds a callback to process the response body before it's sent. These are processed in the order
* they are added
*
* @param callable $callback The callback to process the response body
*
* @return void
*/
public function addResponseBodyCallback(callable $callback): void
{
$this->responseBodyCallbacks[] = $callback;
}
/**
* Cycles through the response body callbacks and processes them in order
*
* @return void
*/
protected function processResponseCallbacks(): void
{
foreach ($this->responseBodyCallbacks as $callback) {
$this->body = $callback($this->body);
}
}
/**
* Downloads a file.
*
* @param string $filePath The path to the file to be downloaded.
*
* @return void
*/
public function downloadFile(string $filePath): void
{
if (file_exists($filePath) === false) {
throw new Exception("$filePath cannot be found.");
}
$fileSize = filesize($filePath);
$mimeType = mime_content_type($filePath);
$mimeType = $mimeType !== false ? $mimeType : 'application/octet-stream';
$this->send();
$this->setRealHeader('Content-Description: File Transfer');
$this->setRealHeader('Content-Type: ' . $mimeType);
$this->setRealHeader('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
$this->setRealHeader('Expires: 0');
$this->setRealHeader('Cache-Control: must-revalidate');
$this->setRealHeader('Pragma: public');
$this->setRealHeader('Content-Length: ' . $fileSize);
// // Clear the output buffer
ob_clean();
flush();
// // Read the file and send it to the output buffer
readfile($filePath);
if (empty(getenv('PHPUNIT_TEST'))) {
exit; // @codeCoverageIgnore
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace flight\net;
use Exception;
class UploadedFile
{
/**
* @var string $name The name of the uploaded file.
*/
private string $name;
/**
* @var string $mimeType The MIME type of the uploaded file.
*/
private string $mimeType;
/**
* @var int $size The size of the uploaded file in bytes.
*/
private int $size;
/**
* @var string $tmpName The temporary name of the uploaded file.
*/
private string $tmpName;
/**
* @var int $error The error code associated with the uploaded file.
*/
private int $error;
/**
* Constructs a new UploadedFile object.
*
* @param string $name The name of the uploaded file.
* @param string $mimeType The MIME type of the uploaded file.
* @param int $size The size of the uploaded file in bytes.
* @param string $tmpName The temporary name of the uploaded file.
* @param int $error The error code associated with the uploaded file.
*/
public function __construct(string $name, string $mimeType, int $size, string $tmpName, int $error)
{
$this->name = $name;
$this->mimeType = $mimeType;
$this->size = $size;
$this->tmpName = $tmpName;
$this->error = $error;
}
/**
* Retrieves the client-side filename of the uploaded file.
*
* @return string The client-side filename.
*/
public function getClientFilename(): string
{
return $this->name;
}
/**
* Retrieves the media type of the uploaded file as provided by the client.
*
* @return string The media type of the uploaded file.
*/
public function getClientMediaType(): string
{
return $this->mimeType;
}
/**
* Returns the size of the uploaded file.
*
* @return int The size of the uploaded file.
*/
public function getSize(): int
{
return $this->size;
}
/**
* Retrieves the temporary name of the uploaded file.
*
* @return string The temporary name of the uploaded file.
*/
public function getTempName(): string
{
return $this->tmpName;
}
/**
* Get the error code associated with the uploaded file.
*
* @return int The error code.
*/
public function getError(): int
{
return $this->error;
}
/**
* Moves the uploaded file to the specified target path.
*
* @param string $targetPath The path to move the file to.
*
* @return void
*/
public function moveTo(string $targetPath): void
{
if ($this->error !== UPLOAD_ERR_OK) {
throw new Exception($this->getUploadErrorMessage($this->error));
}
$isUploadedFile = is_uploaded_file($this->tmpName) === true;
if (
$isUploadedFile === true
&&
move_uploaded_file($this->tmpName, $targetPath) === false
) {
throw new Exception('Cannot move uploaded file'); // @codeCoverageIgnore
} elseif ($isUploadedFile === false && getenv('PHPUNIT_TEST')) {
rename($this->tmpName, $targetPath);
}
}
/**
* Retrieves the error message for a given upload error code.
*
* @param int $error The upload error code.
*
* @return string The error message.
*/
protected function getUploadErrorMessage(int $error): string
{
switch ($error) {
case UPLOAD_ERR_INI_SIZE:
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
case UPLOAD_ERR_FORM_SIZE:
return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
case UPLOAD_ERR_PARTIAL:
return 'The uploaded file was only partially uploaded.';
case UPLOAD_ERR_NO_FILE:
return 'No file was uploaded.';
case UPLOAD_ERR_NO_TMP_DIR:
return 'Missing a temporary folder.';
case UPLOAD_ERR_CANT_WRITE:
return 'Failed to write file to disk.';
case UPLOAD_ERR_EXTENSION:
return 'A PHP extension stopped the file upload.';
default:
return 'An unknown error occurred. Error code: ' . $error;
}
}
}

View File

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

View File

@ -1,74 +1,75 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
declare(strict_types=1);
namespace flight\util;
if (!interface_exists('JsonSerializable')) {
require_once dirname(__FILE__) . '/LegacyJsonSerializable.php';
}
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* The Collection class allows you to access a set of data
* using both array and object notation.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @implements ArrayAccess<string, mixed>
* @implements Iterator<string, mixed>
*/
class Collection implements \ArrayAccess, \Iterator, \Countable, \JsonSerializable {
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
{
/**
* Collection data.
*
* @var array
* @var array<string, mixed>
*/
private $data;
private array $data;
/**
* Constructor.
*
* @param array $data Initial data
* @param array<string, mixed> $data Initial data
*/
public function __construct(array $data = array()) {
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Gets an item.
*
* @param string $key Key
* @return mixed Value
* @return mixed Value if `$key` exists in collection data, otherwise returns `NULL`
*/
public function __get($key) {
return isset($this->data[$key]) ? $this->data[$key] : null;
public function __get(string $key)
{
return $this->data[$key] ?? null;
}
/**
* Set an item.
*
* @param string $key Key
* @param mixed $value Value
* @param mixed $value Value
*/
public function __set($key, $value) {
public function __set(string $key, $value): void
{
$this->data[$key] = $value;
}
/**
* Checks if an item exists.
*
* @param string $key Key
* @return bool Item status
*/
public function __isset($key) {
public function __isset(string $key): bool
{
return isset($this->data[$key]);
}
/**
* Removes an item.
*
* @param string $key Key
*/
public function __unset($key) {
public function __unset(string $key): void
{
unset($this->data[$key]);
}
@ -76,23 +77,27 @@ class Collection implements \ArrayAccess, \Iterator, \Countable, \JsonSerializab
* Gets an item at the offset.
*
* @param string $offset Offset
*
* @return mixed Value
*/
public function offsetGet($offset) {
return isset($this->data[$offset]) ? $this->data[$offset] : null;
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->data[$offset] ?? null;
}
/**
* Sets an item at the offset.
*
* @param string $offset Offset
* @param mixed $value Value
* @param ?string $offset Offset
* @param mixed $value Value
*/
public function offsetSet($offset, $value) {
if (is_null($offset)) {
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value): void
{
if ($offset === null) {
$this->data[] = $value;
}
else {
} else {
$this->data[$offset] = $value;
}
}
@ -100,117 +105,119 @@ class Collection implements \ArrayAccess, \Iterator, \Countable, \JsonSerializab
/**
* Checks if an item exists at the offset.
*
* @param string $offset Offset
* @return bool Item status
* @param string $offset
*/
public function offsetExists($offset) {
public function offsetExists($offset): bool
{
return isset($this->data[$offset]);
}
/**
* Removes an item at the offset.
*
* @param string $offset Offset
* @param string $offset
*/
public function offsetUnset($offset) {
public function offsetUnset($offset): void
{
unset($this->data[$offset]);
}
/**
* Resets the collection.
*/
public function rewind() {
public function rewind(): void
{
reset($this->data);
}
/**
* Gets current collection item.
*
* @return mixed Value
*/
public function current() {
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->data);
}
/**
* Gets current collection key.
*
* @return mixed Value
*/
public function key() {
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->data);
}
/**
* Gets the next collection value.
*
* @return mixed Value
*/
public function next()
*/
#[\ReturnTypeWillChange]
public function next(): void
{
return next($this->data);
next($this->data);
}
/**
* Checks if the current collection key is valid.
*
* @return bool Key status
*/
public function valid()
*/
public function valid(): bool
{
$key = key($this->data);
return ($key !== NULL && $key !== FALSE);
return key($this->data) !== null;
}
/**
* Gets the size of the collection.
*
* @return int Collection size
*/
public function count() {
return sizeof($this->data);
public function count(): int
{
return \count($this->data);
}
/**
* Gets the item keys.
*
* @return array Collection keys
* @return array<int, string> Collection keys
*/
public function keys() {
public function keys(): array
{
return array_keys($this->data);
}
/**
* Gets the collection data.
*
* @return array Collection data
* @return array<string, mixed> Collection data
*/
public function getData() {
public function getData(): array
{
return $this->data;
}
/**
* Sets the collection data.
*
* @param array $data New collection data
* @param array<string, mixed> $data New collection data
*/
public function setData(array $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 <b>json_encode</b>
*/
public function jsonSerialize() {
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->data;
}
/**
* Removes all items from the collection.
*/
public function clear() {
$this->data = array();
public function clear(): void
{
$this->data = [];
}
}

View File

@ -1,11 +0,0 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license MIT, http://flightphp.com/license
*/
interface JsonSerializable {
public function jsonSerialize();
}

View File

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