Le commit un peu tardif de l'api

This commit is contained in:
Fauvet Matthis 2024-06-27 10:07:19 +02:00
parent a0b5103978
commit 35a6e4266f
22 changed files with 4531 additions and 0 deletions

58
api/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,58 @@
## Contributing to the Flight Framework
Thanks for being willing to contribute to the Flight! The goal of Flight is to keep the implementation of things simple and free of outside dependencies.
You should only bring in the depedencies you want in your project right? Right.
### Overarching Guidelines
Flight aims to be simple and fast. Anything that compromises either of those two things will be heavily scrutinized and/or rejected. Other things to consider when making a contribution:
* **Dependencies** - We strive to be dependency free in Flight. Yes even polyfills, yes even `Interface` only repos like `psr/container`. The fewer dependencies, the fewer your exposed attack vectors.
* **Coding Standards** - We use PSR1 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are:
* PHPStan is at level 6.
* `===` instead of truthy or falsey statements like `==` or `!is_array()`.
* **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4.
* **Core functionality vs Plugin** - Have a conversation with us in the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) to know if your idea is worth makes sense in the framework or in a plugin.
* **Testing** - Until automated testing is put into place, any PRs must pass unit testing in PHP 7.4 and PHP 8.2+. Additionally you need to run `composer test-server` and `composer test-server-v2` and ensure all the header links work correctly.
#### **Did you find a bug?**
* **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely pass in the information related to the security vuln.
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/flightphp/core/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
#### **Did you write a patch that fixes a bug?**
* Open a new GitHub pull request with the patch.
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Flight will generally not be accepted.
#### **Do you intend to add a new feature or change an existing one?**
* Hop into the [chatroom](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host) for Flight and let's have a conversation about the feature you want to add. It could be amazing, or it might make more sense as an extension/plugin. If you create a PR without having a conversation with maintainers, it likely will be closed without review.
* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes.
#### **Do you have questions about the source code?**
* Ask any question about how to use Flight in the in the [Flight Matrix chat room](https://matrix.to/#/!cTfwPXhpkTXPXwVmxY:matrix.org?via=matrix.org&via=leitstelle511.net&via=integrations.ems.host).
#### **Do you want to contribute to the Flight documentation?**
* Please see the [Flight Documentation repo on GitHub](https://github.com/flightphp/docs).
Flight is a volunteer effort. We encourage you to pitch in and join!
Thanks! :heart: :heart: :heart:
Flight Team

21
api/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2011 Mike Cao <mike@mikecao.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

59
api/README.md Normal file
View File

@ -0,0 +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
quickly and easily build RESTful web applications.
# Basic Usage
```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();
```
## 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
> [!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://docs.flightphp.com/license) license.

BIN
api/composer.json Normal file

Binary file not shown.

942
api/flight/Engine.php Normal file

File diff suppressed because it is too large Load Diff

146
api/flight/Flight.php Normal file
View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
use flight\Engine;
use flight\net\Request;
use flight\net\Response;
use flight\net\Router;
use flight\template\View;
use flight\net\Route;
require_once __DIR__ . '/autoload.php';
/**
* The Flight class is a static representation of the framework.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*
* # 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.
*
* # 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 Router router() Returns Router instance.
* @method static string getUrl(string $alias, array<string, mixed> $params = []) Gets a url from an alias
*
* @method static void map(string $name, callable $callback) Creates a custom framework method.
*
* @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.
*
* @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.
*
* # 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.
*
* # 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 caching
* @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.
*/
class Flight
{
/** Framework engine. */
private static Engine $engine;
/** 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<int, mixed> $params Method parameters
*
* @return mixed Callback results
* @throws Exception
*/
public static function __callStatic(string $name, array $params)
{
return self::app()->{$name}(...$params);
}
/** @return Engine Application instance */
public static function app(): Engine
{
if (!self::$initialized) {
require_once __DIR__ . '/autoload.php';
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;
}
}

10
api/flight/autoload.php Normal file
View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
use flight\core\Loader;
require_once __DIR__ . '/Flight.php';
require_once __DIR__ . '/core/Loader.php';
Loader::autoload(true, [dirname(__DIR__)]);

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;
}
}

File diff suppressed because it is too large Load Diff

241
api/flight/core/Loader.php Normal file
View File

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace flight\core;
use Closure;
use Exception;
/**
* The Loader class is responsible for loading objects. It maintains
* a list of reusable class instances and can generate a new class
* instances with custom initialization parameters. It also performs
* class autoloading.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Loader
{
/**
* Registered classes.
*
* @var array<string, array{class-string|Closure(): object, array<int, mixed>, ?callable}> $classes
*/
protected array $classes = [];
/**
* If this is disabled, classes can load with underscores
*/
protected static bool $v2ClassLoading = true;
/**
* Class instances.
*
* @var array<string, object>
*/
protected array $instances = [];
/**
* Autoload directories.
*
* @var array<int, string>
*/
protected static array $dirs = [];
/**
* Registers a class.
*
* @param string $name Registry name
* @param class-string<T>|Closure(): T $class Class name or function to instantiate class
* @param array<int, mixed> $params Class initialization parameters
* @param ?Closure(T $instance): void $callback $callback Function to call after object instantiation
*
* @template T of object
*/
public function register(string $name, $class, array $params = [], ?callable $callback = null): void
{
unset($this->instances[$name]);
$this->classes[$name] = [$class, $params, $callback];
}
/**
* Unregisters a class.
*
* @param string $name Registry name
*/
public function unregister(string $name): void
{
unset($this->classes[$name]);
}
/**
* Loads a registered class.
*
* @param string $name Method name
* @param bool $shared Shared instance
*
* @throws Exception
*
* @return ?object Class instance
*/
public function load(string $name, bool $shared = true): ?object
{
$obj = null;
if (isset($this->classes[$name])) {
[0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name];
$exists = isset($this->instances[$name]);
if ($shared) {
$obj = ($exists) ?
$this->getInstance($name) :
$this->newInstance($class, $params);
if (!$exists) {
$this->instances[$name] = $obj;
}
} else {
$obj = $this->newInstance($class, $params);
}
if ($callback && (!$shared || !$exists)) {
$ref = [&$obj];
\call_user_func_array($callback, $ref);
}
}
return $obj;
}
/**
* Gets a single instance of a class.
*
* @param string $name Instance name
*
* @return ?object Class instance
*/
public function getInstance(string $name): ?object
{
return $this->instances[$name] ?? null;
}
/**
* Gets a new instance of a class.
*
* @param class-string<T>|Closure(): class-string<T> $class Class name or callback function to instantiate class
* @param array<int, string> $params Class initialization parameters
*
* @template T of object
*
* @throws Exception
*
* @return T Class instance
*/
public function newInstance($class, array $params = [])
{
if (\is_callable($class)) {
return \call_user_func_array($class, $params);
}
return new $class(...$params);
}
/**
* Gets a registered callable
*
* @param string $name Registry name
*
* @return mixed Class information or null if not registered
*/
public function get(string $name)
{
return $this->classes[$name] ?? null;
}
/**
* Resets the object to the initial state.
*/
public function reset(): void
{
$this->classes = [];
$this->instances = [];
}
// Autoloading Functions
/**
* Starts/stops autoloader.
*
* @param bool $enabled Enable/disable autoloading
* @param string|iterable<int, string> $dirs Autoload directories
*/
public static function autoload(bool $enabled = true, $dirs = []): void
{
if ($enabled) {
spl_autoload_register([__CLASS__, 'loadClass']);
} else {
spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore
}
if (!empty($dirs)) {
self::addDirectory($dirs);
}
}
/**
* Autoloads classes.
*
* Classes are not allowed to have underscores in their names.
*
* @param string $class Class name
*/
public static function loadClass(string $class): void
{
$replace_chars = self::$v2ClassLoading === true ? ['\\', '_'] : ['\\'];
$classFile = str_replace($replace_chars, '/', $class) . '.php';
foreach (self::$dirs as $dir) {
$filePath = "$dir/$classFile";
if (file_exists($filePath)) {
require_once $filePath;
return;
}
}
}
/**
* Adds a directory for autoloading classes.
*
* @param string|iterable<int, string> $dir Directory path
*/
public static function addDirectory($dir): void
{
if (\is_array($dir) || \is_object($dir)) {
foreach ($dir as $value) {
self::addDirectory($value);
}
} elseif (\is_string($dir)) {
if (!\in_array($dir, self::$dirs, true)) {
self::$dirs[] = $dir;
}
}
}
/**
* Sets the value for V2 class loading.
*
* @param bool $value The value to set for V2 class loading.
*
* @return void
*/
public static function setV2ClassLoading(bool $value): void
{
self::$v2ClassLoading = $value;
}
}

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];
}
}

417
api/flight/net/Request.php Normal file
View File

@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace flight\net;
use flight\util\Collection;
/**
* The Request class represents an HTTP request. Data from
* 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
*/
class Request
{
/**
* URL being requested
*/
public string $url;
/**
* Parent subdirectory of the URL
*/
public string $base;
/**
* Request method (GET, POST, PUT, DELETE)
*/
public string $method;
/**
* Referrer URL
*/
public string $referrer;
/**
* IP address of the client
*/
public string $ip;
/**
* Whether the request is an AJAX request
*/
public bool $ajax;
/**
* Server protocol (http, https)
*/
public string $scheme;
/**
* Browser information
*/
public string $user_agent;
/**
* Content type
*/
public string $type;
/**
* Content length
*/
public int $length;
/**
* Query string parameters
*/
public Collection $query;
/**
* Post parameters
*/
public Collection $data;
/**
* Cookie parameters
*/
public Collection $cookies;
/**
* Uploaded files
*/
public Collection $files;
/**
* Whether the connection is secure
*/
public bool $secure;
/**
* HTTP accept parameters
*/
public string $accept;
/**
* Proxy IP address of the client
*/
public string $proxy_ip;
/**
* HTTP host name
*/
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<string, mixed> $config Request configuration
*/
public function __construct(array $config = [])
{
// Default properties
if (empty($config)) {
$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' => 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);
}
/**
* Initialize request properties.
*
* @param array<string, mixed> $properties Array of request properties
*
* @return self
*/
public function init(array $properties = []): self
{
// Set all the defined properties
foreach ($properties as $name => $value) {
$this->{$name} = $value;
}
// Get the requested URL without the base directory
// This rewrites the url in case the public url and base directories match
// (such as installing on a subdirectory in a web server)
// @see testInitUrlSameAsBaseDirectory
if ($this->base !== '/' && $this->base !== '' && strpos($this->url, $this->base) === 0) {
$this->url = substr($this->url, \strlen($this->base));
}
// Default url
if (empty($this->url) === true) {
$this->url = '/';
} else {
// Merge URL query parameters with $_GET
$_GET = array_merge($_GET, self::parseQuery($this->url));
$this->query->setData($_GET);
}
// Check for JSON input
if (strpos($this->type, 'application/json') === 0) {
$body = $this->getBody();
if ($body !== '') {
$data = json_decode($body, true);
if (is_array($data) === true) {
$this->data->setData($data);
}
}
}
return $this;
}
/**
* Gets the body of the request.
*
* @return string Raw HTTP request body
*/
public function getBody(): string
{
$body = $this->body;
if ($body !== '') {
return $body;
}
$method = $this->method ?? self::getMethod();
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.
*/
public static function getMethod(): string
{
$method = self::getVar('REQUEST_METHOD', 'GET');
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) === true) {
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
} elseif (isset($_REQUEST['_method']) === true) {
$method = $_REQUEST['_method'];
}
return strtoupper($method);
}
/**
* Gets the real remote IP address.
*
* @return string IP address
*/
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',
];
$flags = \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE;
foreach ($forwarded as $key) {
if (\array_key_exists($key, $_SERVER) === true) {
sscanf($_SERVER[$key], '%[^,]', $ip);
if (filter_var($ip, \FILTER_VALIDATE_IP, $flags) !== false) {
return $ip;
}
}
}
return '';
}
/**
* Gets a variable from $_SERVER using $default if not provided.
*
* @param string $var Variable name
* @param mixed $default Default value to substitute
*
* @return mixed Server variable value
*/
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<string, int|string|array<int|string, int|string>>
*/
public static function parseQuery(string $url): array
{
$params = [];
$args = parse_url($url);
if (isset($args['query']) === true) {
parse_str($args['query'], $params);
}
return $params;
}
/**
* Gets the URL Scheme
*
* @return string 'http'|'https'
*/
public static function getScheme(): string
{
if (
(isset($_SERVER['HTTPS']) === true && strtolower($_SERVER['HTTPS']) === 'on')
||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) === true && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
||
(isset($_SERVER['HTTP_FRONT_END_HTTPS']) === true && $_SERVER['HTTP_FRONT_END_HTTPS'] === 'on')
||
(isset($_SERVER['REQUEST_SCHEME']) === true && $_SERVER['REQUEST_SCHEME'] === 'https')
) {
return 'https';
}
return 'http';
}
}

473
api/flight/net/Response.php Normal file
View File

@ -0,0 +1,473 @@
<?php
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
{
/**
* Content-Length header.
*/
public bool $content_length = true;
/**
* This is to maintain legacy handling of output buffering
* which causes a lot of problems. This will be removed
* in v4
*
* @var boolean
*/
public bool $v2_output_buffering = false;
/**
* HTTP status codes
*
* @var array<int, ?string> $codes
*/
public static array $codes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
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.
*
* @throws Exception If invalid status code
*
* @return int|$this Self reference
*/
public function status(?int $code = null)
{
if ($code === null) {
return $this->status;
}
if (\array_key_exists($code, self::$codes)) {
$this->status = $code;
} else {
throw new Exception('Invalid status code.');
}
return $this;
}
/**
* 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 header($name, ?string $value = null): self
{
if (\is_array($name)) {
foreach ($name as $k => $v) {
$this->headers[$k] = $v;
}
} else {
$this->headers[$name] = $value;
}
return $this;
}
/**
* Gets a single header from the response.
*
* @param string $name the name of the header
*
* @return string|null
*/
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
* @param bool $overwrite Overwrite the response body
*
* @return $this Self reference
*/
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 $this Self reference
*/
public function clear(): self
{
$this->status = 200;
$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;
}
/**
* Sets caching headers for the response.
*
* @param int|string|false $expires Expiration time as time() or as strtotime() string value
*
* @return $this Self reference
*/
public function cache($expires): self
{
if ($expires === false) {
$this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
$this->headers['Cache-Control'] = [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
];
$this->headers['Pragma'] = 'no-cache';
} 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') {
unset($this->headers['Pragma']);
}
}
return $this;
}
/**
* Sends HTTP headers.
*
* @return $this Self reference
*/
public function sendHeaders(): self
{
// Send status code header
if (strpos(\PHP_SAPI, 'cgi') !== false) {
// @codeCoverageIgnoreStart
$this->setRealHeader(
sprintf(
'Status: %d %s',
$this->status,
self::$codes[$this->status]
),
true
);
// @codeCoverageIgnoreEnd
} else {
$this->setRealHeader(
sprintf(
'%s %d %s',
$_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
$this->status,
self::$codes[$this->status]
),
true,
$this->status
);
}
if ($this->content_length === true) {
// Send content length
$length = $this->getContentLength();
if ($length > 0) {
$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);
}
}
return $this;
}
/**
* Sets a real header. Mostly used for test mocking.
*
* @param string $header_string The header string you would pass to header()
* @param bool $replace The optional replace parameter indicates whether the
* header should replace a previous similar header, or add a second header of
* the same type. By default it will replace, but if you pass in false as the
* second argument you can force multiple headers of the same type.
* @param int $response_code The response code to send
*
* @return self
*
* @codeCoverageIgnore
*/
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self
{
header($header_string, $replace, $response_code);
return $this;
}
/**
* Gets the content length.
*/
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(): void
{
// legacy way of handling this
if ($this->v2_output_buffering === true) {
if (ob_get_length() > 0) {
ob_end_clean(); // @codeCoverageIgnore
}
}
// Only for the v3 output buffering.
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
}
if (headers_sent() === false) {
$this->sendHeaders(); // @codeCoverageIgnore
}
echo $this->body;
$this->sent = true;
}
/**
* 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);
}
}
}

266
api/flight/net/Route.php Normal file
View File

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
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
{
/**
* URL pattern
*/
public string $pattern;
/**
* Callback function
*
* @var mixed
*/
public $callback;
/**
* HTTP methods
*
* @var array<int, string>
*/
public array $methods = [];
/**
* Route parameters
*
* @var array<int, ?string>
*/
public array $params = [];
/**
* Matching regular expression
*/
public ?string $regex = null;
/**
* URL splat content
*/
public string $splat = '';
/**
* Pass self in callback parameters
*/
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 callable|string $callback Callback function
* @param array<int, string> $methods HTTP methods
* @param bool $pass Pass self in callback parameters
*/
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 (original format, not URL decoded)
* @param bool $case_sensitive Case sensitive matching
*
* @return bool Match status
*/
public function matchUrl(string $url, bool $case_sensitive = false): bool
{
// Wildcard or exact match
if ($this->pattern === '*' || $this->pattern === $url) {
return true;
}
$ids = [];
$last_char = substr($this->pattern, -1);
// Get splat
if ($last_char === '*') {
$n = 0;
$len = \strlen($url);
$count = substr_count($this->pattern, '/');
for ($i = 0; $i < $len; $i++) {
if ($url[$i] === '/') {
++$n;
}
if ($n === $count) {
break;
}
}
$this->splat = urldecode(strval(substr($url, $i + 1)));
}
// Build the regex for matching
$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]+)(:([^/\(\)]*))?#',
static function ($matches) use (&$ids) {
$ids[$matches[1]] = null;
if (isset($matches[3])) {
return '(?P<' . $matches[1] . '>' . $matches[3] . ')';
}
return '(?P<' . $matches[1] . '>[^/\?]+)';
},
$regex
);
$regex .= $last_char === '/' ? '?' : '/?';
// Attempt to match route and named parameters
if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) {
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(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;
}
}

330
api/flight/net/Router.php Normal file
View File

@ -0,0 +1,330 @@
<?php
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.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*/
class Router
{
/**
* Case sensitive matching.
*/
public bool $case_sensitive = false;
/**
* Mapped routes.
*
* @var array<int,Route> $routes
*/
protected array $routes = [];
/**
* The current route that is has been found and executed.
*/
public ?Route $executedRoute = null;
/**
* Pointer to current route.
*/
protected int $index = 0;
/**
* When groups are used, this is mapped against all the routes
*/
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<int,Route> Array of routes
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* Clears all routes in the router.
*/
public function clear(): void
{
$this->routes = [];
}
/**
* Maps a URL pattern to a callback function.
*
* @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(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route
{
// This means that the route ies 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);
}
$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.
*
* @return false|Route Matching route or false if no match
*/
public function route(Request $request)
{
while ($route = $this->current()) {
$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();
}
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);
}
/**
* 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 false|Route
*/
public function current()
{
return $this->routes[$this->index] ?? false;
}
/**
* Gets the previous route.
*/
public function previous(): void
{
--$this->index;
}
/**
* Gets the next route.
*/
public function next(): void
{
++$this->index;
}
/**
* Reset to the first route.
*/
public function reset(): void
{
$this->rewind();
}
}

View File

@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
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. */
public string $path;
/** File extension. */
public string $extension = '.php';
public bool $preserveVars = true;
/**
* View variables.
*
* @var array<string, mixed> $vars
*/
protected array $vars = [];
/** Template file. */
private string $template;
/**
* Constructor.
*
* @param string $path Path to templates directory
*/
public function __construct(string $path = '.')
{
$this->path = $path;
}
/**
* Gets a template variable.
*
* @return mixed Variable value or `null` if doesn't exists
*/
public function get(string $key)
{
return $this->vars[$key] ?? null;
}
/**
* Sets a template variable.
*
* @param string|iterable<string, mixed> $key
* @param mixed $value Value
*
* @return self
*/
public function set($key, $value = null): self
{
if (\is_iterable($key)) {
foreach ($key as $k => $v) {
$this->vars[$k] = $v;
}
} else {
$this->vars[$key] = $value;
}
return $this;
}
/**
* Checks if a template variable is set.
*
* @return bool If key exists
*/
public function has(string $key): bool
{
return isset($this->vars[$key]);
}
/**
* Unsets a template variable. If no key is passed in, clear all variables.
*
* @return $this
*/
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<string, mixed> $data Template data
*
* @throws \Exception If template not found
*/
public function render(string $file, ?array $data = null): void
{
$this->template = $this->getTemplate($file);
if (!\file_exists($this->template)) {
$normalized_path = self::normalizePath($this->template);
throw new \Exception("Template file not found: {$normalized_path}.");
}
\extract($this->vars);
if (\is_array($data) === true) {
\extract($data);
if ($this->preserveVars === true) {
$this->vars = \array_merge($this->vars, $data);
}
}
include $this->template;
}
/**
* Gets the output of a template.
*
* @param string $file Template file
* @param ?array<string, mixed> $data Template data
*
* @return string Output of template
*/
public function fetch(string $file, ?array $data = null): string
{
\ob_start();
$this->render($file, $data);
return \ob_get_clean();
}
/**
* Checks if a template file exists.
*
* @param string $file Template file
*
* @return bool Template file exists
*/
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(string $file): string
{
$ext = $this->extension;
if (!empty($ext) && (\substr($file, -1 * \strlen($ext)) != $ext)) {
$file .= $ext;
}
$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 . DIRECTORY_SEPARATOR . $file;
}
/**
* Displays escaped output.
*
* @param string $str String to escape
*
* @return string Escaped string
*/
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

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace flight\util;
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
{
/**
* Collection data.
*
* @var array<string, mixed>
*/
private array $data;
/**
* Constructor.
*
* @param array<string, mixed> $data Initial data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Gets an item.
*
* @return mixed Value if `$key` exists in collection data, otherwise returns `NULL`
*/
public function __get(string $key)
{
return $this->data[$key] ?? null;
}
/**
* Set an item.
*
* @param mixed $value Value
*/
public function __set(string $key, $value): void
{
$this->data[$key] = $value;
}
/**
* Checks if an item exists.
*/
public function __isset(string $key): bool
{
return isset($this->data[$key]);
}
/**
* Removes an item.
*/
public function __unset(string $key): void
{
unset($this->data[$key]);
}
/**
* Gets an item at the offset.
*
* @param string $offset Offset
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->data[$offset] ?? null;
}
/**
* Sets an item at the offset.
*
* @param ?string $offset Offset
* @param mixed $value Value
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value): void
{
if ($offset === null) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
/**
* Checks if an item exists at the offset.
*
* @param string $offset
*/
public function offsetExists($offset): bool
{
return isset($this->data[$offset]);
}
/**
* Removes an item at the offset.
*
* @param string $offset
*/
public function offsetUnset($offset): void
{
unset($this->data[$offset]);
}
/**
* Resets the collection.
*/
public function rewind(): void
{
reset($this->data);
}
/**
* Gets current collection item.
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->data);
}
/**
* Gets current collection key.
*
* @return mixed Value
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->data);
}
/**
* Gets the next collection value.
*/
#[\ReturnTypeWillChange]
public function next(): void
{
next($this->data);
}
/**
* Checks if the current collection key is valid.
*/
public function valid(): bool
{
return key($this->data) !== null;
}
/**
* Gets the size of the collection.
*/
public function count(): int
{
return \count($this->data);
}
/**
* Gets the item keys.
*
* @return array<int, string> Collection keys
*/
public function keys(): array
{
return array_keys($this->data);
}
/**
* Gets the collection data.
*
* @return array<string, mixed> Collection data
*/
public function getData(): array
{
return $this->data;
}
/**
* Sets the collection data.
*
* @param array<string, mixed> $data New collection data
*/
public function setData(array $data): void
{
$this->data = $data;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->data;
}
/**
* Removes all items from the collection.
*/
public function clear(): void
{
$this->data = [];
}
}

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
{
}

79
api/index.php Normal file
View File

@ -0,0 +1,79 @@
<?php
require 'flight/Flight.php';
require 'model/model.php';
MusicAPI::init();
Flight::route('GET /songs(/@id)', 'findSong');
Flight::route('GET /albums(/@id)', 'findAlbum');
Flight::route('GET /artists(/@id)', 'findArtist');
function findSong($id = null)
{
$title = Flight::request()->query->title ?? false;
if ($id === null) {
if($title) {
$res = MusicAPI::findAllSongByTitleContaining($title);
} else {
$res = MusicAPI::findAllSong();
}
Flight::json(["results" => $res]);
} else {
$res = MusicAPI::findSongById($id);
if ($res)
{
Flight::json($res);
} else {
Flight::halt(404);
}
}
}
function findAlbum($id = null)
{
$name = Flight::request()->query->name ?? false;
if ($id === null) {
if ($name){
$res = MusicAPI::findAlbumByNameContaining($name);
} else {
$res = MusicAPI::findAllAlbum();
}
Flight::json(["results" => $res]);
} else {
$res = MusicAPI::findAlbumById($id);
if ($res) {
Flight::json($res);
} else {
Flight::halt(404);
}
}
}
function findArtist($id = null)
{
$name = Flight::request()->query->name ?? false;
if ($id === null) {
if ($name){
$res = MusicAPI::findArtsistByNameContaining($name);
} else {
$res = MusicAPI::findAllArtist();
}
Flight::json(["results" => $res]);
}
else {
$res = MusicAPI::findArtsistById($id);
if ($res) {
Flight::json($res);
} else {
Flight::halt(404);
}
}
}
Flight::start();
?>

131
api/model/model.php Normal file
View File

@ -0,0 +1,131 @@
<?php
class Database
{
private $host;
private $user;
private $pass;
private $dbname;
private $pdo;
public function __construct($host, $user, $pass, $dbname) {
$this->host = $host;
$this->user = $user;
$this->pass = $pass;
$this->dbname = $dbname;
}
public function connect() {
$driver_options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$dsn = "mysql:host=$this->host;dbname=$this->dbname";
$this->pdo = new PDO($dsn, $this->user, $this->pass,$driver_options);
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
if ($stmt->execute($params) === true)
return $stmt;
return false;
}
public function lastInsertId(){
return $this->pdo->lastInsertId();
}
}
class MusicAPI
{
static private $db = null;
static private $BASE_QUERY = "SELECT Song.id, Song.name, Album.name AS `album-name`, Album.year, Artist.name AS `artist-name`, Genre.name AS `genre` ".
"FROM Song ".
"JOIN Track ON Song.id=Track.songId ".
"JOIN Album ON Track.albumId=Album.id ".
"JOIN Artist ON Album.artistId=Artist.id ".
"JOIN Cover ON Album.coverId=Cover.id ".
"JOIN Genre ON Album.genreId=Genre.id ";
public static function init()
{
self::$db = new Database("localhost", "fauvet", "toto", "fauvet");
self::$db->connect();
}
public static function findSongById($id)
{
$sql = self::$BASE_QUERY."WHERE Song.id = ?";
$stmt = self::$db->query($sql, [$id]);
return $stmt->fetchAll();
}
public static function findAllSongByTitleContaining($title)
{
$sql = self::$BASE_QUERY."WHERE Song.name LIKE ?;";
$stmt = self::$db->query($sql, ['%' . $title . '%']);
return $stmt->fetchAll();
}
public static function findSongByAlbumName($a)
{
$sql = self::$BASE_QUERY."WHERE Album.name LIKE ?;";
$stmt = self::$db->query($sql, ['%' . $a . '%']);
return $stmt->fetchAll();
}
public static function findAllSong()
{
$sql = self::$BASE_QUERY.";";
$stmt = self::$db->query($sql);
return $stmt->fetchAll();
}
//Fonctions pour la recherche d'album
public static function findAlbumById($id)
{
$sql = "SELECT Album.id, Album.name, Album.year FROM Album WHERE Album.id=?;";
$stmt = self::$db->query($sql, [$id]);
return $stmt->fetchAll();
}
public static function findAlbumByNameContaining($n)
{
$sql = "SELECT Album.id, Album.name, Album.year FROM Album WHERE Album.name LIKE ?;";
$stmt = self::$db->query($sql, ['%'.$n.'%']);
return $stmt->fetchAll();
}
public static function findAllAlbum()
{
$sql = "SELECT Album.id, Album.name, Album.year FROM Album;";
$stmt = self::$db->query($sql);
return $stmt->fetchAll();
}
//Fonctions pour la recherche de titres
public static function findArtsistById($id)
{
$sql = "SELECT Artist.id, Artist.name FROM Artist WHERE Artist.id=?;";
$stmt = self::$db->query($sql, [$id]);
return $stmt->fetchAll();
}
public static function findArtsistByNameContaining($n)
{
$sql = "SELECT Artist.id, Artist.name FROM Artist WHERE Artist.name LIKE ?;";
$stmt = self::$db->query($sql, ['%'.$n.'%']);
return $stmt->fetchAll();
}
public static function findAllArtist()
{
$sql = "SELECT * FROM Artist;";
$stmt = self::$db->query($sql);
return $stmt->fetchAll();
}
}
?>

53
api/phpcs.xml.dist Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd"
name="pcsg-generated-ruleset">
<description>
Created with the PHP Coding Standard Generator.
http://edorian.github.io/php-coding-standard-generator/
</description>
<arg name="colors" />
<arg name="tab-width" value="4" />
<rule ref="PSR1">
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
</rule>
<rule ref="PSR12" />
<rule ref="Generic">
<exclude name="Generic.PHP.ClosingPHPTag.NotFound" />
<exclude name="Generic.PHP.UpperCaseConstant.Found" />
<exclude name="Generic.Arrays.DisallowShortArraySyntax.Found" />
<exclude name="Generic.Files.EndFileNoNewline.Found" />
<exclude name="Generic.Files.LowercasedFilename.NotFound" />
<exclude name="Generic.Commenting.DocComment.TagValueIndent" />
<exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine" />
<exclude name="Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed" />
<exclude name="Generic.Commenting.DocComment.ContentAfterOpen" />
<exclude name="Generic.Commenting.DocComment.ContentBeforeClose" />
<exclude name="Generic.Commenting.DocComment.MissingShort" />
<exclude name="Generic.Commenting.DocComment.SpacingBeforeShort" />
<exclude name="Generic.Formatting.NoSpaceAfterCast.SpaceFound" />
<exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine" />
<exclude name="Generic.Formatting.MultipleStatementAlignment.NotSameWarning" />
<exclude name="Generic.Functions.OpeningFunctionBraceBsdAllman.BraceOnSameLine" />
<exclude name="Generic.PHP.DisallowRequestSuperglobal.Found" />
</rule>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="ignoreComments" value="true" />
</properties>
</rule>
<rule ref="Generic.Formatting.SpaceAfterNot">
<properties>
<property name="spacing" value="0" />
</properties>
</rule>
<rule ref="Generic.WhiteSpace.ArbitraryParenthesesSpacing">
<properties>
<property name="ignoreNewlines" value="true" />
</properties>
</rule>
<file>flight/</file>
<file>tests/</file>
<exclude-pattern>tests/views/*</exclude-pattern>
</ruleset>