Merge branch 'master' into replace-table-with-chart-in-readme

pull/718/head
n0nag0n 1 day ago committed by GitHub
commit cb27287bbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2` - Run test server: `composer test-server` or `composer test-server-v2`
- Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) - Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Test coverage: `composer test-coverage` - Test coverage: `composer test-coverage`
## Coding Standards ## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS) - Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`) - Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance - PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2` - Run test server: `composer test-server` or `composer test-server-v2`
- Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) - Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Test coverage: `composer test-coverage` - Test coverage: `composer test-coverage`
## Coding Standards ## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS) - Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`) - Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance - PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

@ -16,11 +16,11 @@ This is the main FlightPHP core library for building fast, simple, and extensibl
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher) - Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
- Run test server: `composer test-server` or `composer test-server-v2` - Run test server: `composer test-server` or `composer test-server-v2`
- Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6) - Lint code & Check code style: `composer lint` (uses phpstan/phpstan, level 6)
- Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR1) - Beautify code: `composer format` (uses squizlabs/php_codesniffer, PSR12)
- Test coverage: `composer test-coverage` - Test coverage: `composer test-coverage`
## Coding Standards ## Coding Standards
- Follow PSR1 coding standards (enforced by PHPCS) - Follow PSR12 coding standards (enforced by PHPCS)
- Use strict comparisons (`===`, `!==`) - Use strict comparisons (`===`, `!==`)
- PHPStan level 6 compliance - PHPStan level 6 compliance
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features) - Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)

@ -9,19 +9,19 @@ Flight aims to be simple and fast. Anything that compromises either of those two
* **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. * **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: * **Coding Standards** - We use PSR-12 coding standards enforced by PHPCS. Some standards that either need additional configuration or need to be manually done are:
* PHPStan is at level 6. * PHPStan is at level 6.
* `===` instead of truthy or falsey statements like `==` or `!is_array()`. * `===` instead of truthy or falsy statements like `==`.
* **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4. * **PHP 7.4 Focused** - We do not make PHP 8+ focused enhancements on the framework as the focus is maintaining PHP 7.4.
* **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. * **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. * **Testing** - PRs must pass unit tests on PHP 7.4 through PHP 8.5+. 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?** #### **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. * **Do not open up a GitHub issue if the bug is a security vulnerability**. Instead contact maintainers directly via email to safely share details about the security vulnerability.
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues). * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/flightphp/core/issues).

@ -22,13 +22,10 @@ composer require flightphp/core
or you can download a zip of this repo. Then you would have a basic `index.php` file like the following: or you can download a zip of this repo. Then you would have a basic `index.php` file like the following:
```php ```php
// if installed with composer require 'flight/autoload.php';
require 'vendor/autoload.php';
// or if installed manually by zip file
// require 'flight/Flight.php';
Flight::route('/', function () { Flight::route('/', function () {
echo 'hello world!'; echo 'hello world!';
}); });
Flight::start(); Flight::start();
@ -38,7 +35,7 @@ Flight::start();
Yes! Flight is fast. It is one of the fastest PHP frameworks available. You can see all the benchmarks at [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=frameworks) Yes! Flight is fast. It is one of the fastest PHP frameworks available. You can see all the benchmarks at [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=frameworks)
See the benchmark below with some other popular PHP frameworks. This is measured in requests processed within the same timeframe. See the benchmark below with some other popular PHP frameworks. This is measured in requests processed within the same timeframe.
```mermaid ```mermaid
--- ---
@ -78,9 +75,10 @@ If you have a current project on v2, you should be able to upgrade to v3 with no
> [!IMPORTANT] > [!IMPORTANT]
> Flight requires `PHP 7.4` or greater. > 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. > [!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.
The framework also supports PHP >8. > Forcing a move to PHP 8 would cause a lot of heartburn for those users.
> The framework also supports PHP 8.
# Roadmap # Roadmap

@ -24,8 +24,7 @@ use Psr\Container\ContainerInterface;
* It is responsible for loading an HTTP request, running the assigned services, * It is responsible for loading an HTTP request, running the assigned services,
* and generating an HTTP response. * and generating an HTTP response.
* *
* @license MIT, https://docs.flightphp.com/license * @copyright 2011-2026, Mike Cao https://mikecao.com, n0nag0n <n0nag0n@sky-9.com>
* @copyright Copyright (c) 2011-2025, Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com>
* *
* @method void start() * @method void start()
* @method void stop() * @method void stop()
@ -75,10 +74,7 @@ use Psr\Container\ContainerInterface;
* @phpstan-method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @phpstan-method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * @phpstan-method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @phpstan-method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* * @license https://docs.flightphp.com/license MIT
* Note: IDEs will use standard @method tags for autocompletion, while PHPStan will use @phpstan-* tags for advanced type checking.
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/ */
class Engine class Engine
{ {
@ -227,8 +223,24 @@ class Engine
// which causes a lot of problems. This will be removed // which causes a lot of problems. This will be removed
// in v4 // in v4
$self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering'); $self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering');
// Propagate method override setting to Request
$self->request()::$allowMethodOverride = (bool) $self->get('flight.allow_method_override'); // Propagate method override setting to Request.
// Assign the static on the class first — do not call request() before this,
// or Request's constructor caches the method while the flag is still the default (true).
Request::$allowMethodOverride = (bool) $self->get('flight.allow_method_override');
// If a Request was already built earlier (common: apps touch request() before start)
// while override was still enabled, refresh the cached verb when override is off
// and an override input is present. When no override input exists, leave any
// intentional manual method assignment alone.
if (Request::$allowMethodOverride === false) {
$hasOverrideInput = Request::getVar('HTTP_X_HTTP_METHOD_OVERRIDE') !== ''
|| isset($_REQUEST['_method']);
if ($hasOverrideInput === true) {
$request = $self->request();
$request->method = Request::getMethod();
}
}
}); });
$this->initialized = true; $this->initialized = true;

@ -15,7 +15,7 @@ use Psr\Container\ContainerInterface;
* The Flight class is a static representation of the framework. * The Flight class is a static representation of the framework.
* *
* @license MIT, https://docs.flightphp.com/license * @license MIT, https://docs.flightphp.com/license
* @copyright Copyright (c) 2011-2025, Mike Cao <mike@mikecao.com>, n0nag0n <n0nag0n@sky-9.com> * @copyright 2011-2026, Mike Cao https://mikecao.com, n0nag0n <n0nag0n@sky-9.com>
* *
* @method static void start() * @method static void start()
* @method static void path(string $dir) * @method static void path(string $dir)
@ -77,11 +77,8 @@ use Psr\Container\ContainerInterface;
* @phpstan-method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* @phpstan-method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * @phpstan-method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @phpstan-method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) * @phpstan-method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* * @license https://docs.flightphp.com/license MIT
* Note: IDEs will use standard @method tags for autocompletion,
* while PHPStan will use @phpstan-* tags for advanced type checking.
*/ */
// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace
class Flight class Flight
{ {
/** /**

@ -18,8 +18,8 @@ use TypeError;
* allows you to hook other functions to an event that can modify the * allows you to hook other functions to an event that can modify the
* input parameters and/or the output. * input parameters and/or the output.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
* @phpstan-template EngineTemplate of object * @phpstan-template EngineTemplate of object
*/ */
class Dispatcher class Dispatcher

@ -6,134 +6,83 @@ namespace flight\core;
class EventDispatcher class EventDispatcher
{ {
/** @var self|null Singleton instance of the EventDispatcher */
private static ?self $instance = null; private static ?self $instance = null;
/** @var array<string, array<int, callable>> */ /** @var array<string, callable[]> */
protected array $listeners = []; protected array $listeners = [];
/**
* Singleton instance of the EventDispatcher.
*
* @return self
*/
public static function getInstance(): self public static function getInstance(): self
{ {
if (self::$instance === null) { if (self::$instance === null) {
self::$instance = new self(); self::$instance = new self();
} }
return self::$instance; return self::$instance;
} }
/**
* Register a callback for an event.
*
* @param string $event Event name
* @param callable $callback Callback function
*/
public function on(string $event, callable $callback): void public function on(string $event, callable $callback): void
{ {
if (isset($this->listeners[$event]) === false) { $this->listeners[$event] ??= [];
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $callback; $this->listeners[$event][] = $callback;
} }
/** /**
* Trigger an event with optional arguments. * @param mixed ...$args Arguments to pass to the listeners.
*
* @param string $event Event name
* @param mixed ...$args Arguments to pass to the callbacks
*
* @return mixed * @return mixed
*/ */
public function trigger(string $event, ...$args) public function trigger(string $event, ...$args)
{ {
$result = null; $listenerReturnValue = null;
if (isset($this->listeners[$event]) === true) {
foreach ($this->listeners[$event] as $callback) { foreach ($this->getListeners($event) as $listener) {
$result = call_user_func_array($callback, $args); $listenerReturnValue = $listener(...$args);
// If you return false, it will break the loop and stop the other event listeners. if ($listenerReturnValue === false) {
if ($result === false) { break;
break; // Stop executing further listeners
}
} }
} }
return $result;
return $listenerReturnValue;
} }
/**
* Check if an event has any registered listeners.
*
* @param string $event Event name
*
* @return bool True if the event has listeners, false otherwise
*/
public function hasListeners(string $event): bool public function hasListeners(string $event): bool
{ {
return isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0; return (
isset($this->listeners[$event])
&& is_array($this->listeners[$event])
&& count($this->listeners[$event])
);
} }
/** /** @return callable[] */
* Get all listeners registered for a specific event.
*
* @param string $event Event name
*
* @return array<int, callable> Array of callbacks registered for the event
*/
public function getListeners(string $event): array public function getListeners(string $event): array
{ {
return $this->listeners[$event] ?? []; return $this->listeners[$event] ?? [];
} }
/** /** @return string[] */
* Get a list of all events that have registered listeners.
*
* @return array<int, string> Array of event names
*/
public function getAllRegisteredEvents(): array public function getAllRegisteredEvents(): array
{ {
return array_keys($this->listeners); return array_keys($this->listeners);
} }
/**
* Remove a specific listener for an event.
*
* @param string $event the event name
* @param callable $callback the exact callback to remove
*
* @return void
*/
public function removeListener(string $event, callable $callback): void public function removeListener(string $event, callable $callback): void
{ {
if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) { if (!$this->hasListeners($event)) {
$this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) { return;
return $listener !== $callback;
});
$this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array
} }
$this->listeners[$event] = array_values(array_filter(
$this->getListeners($event),
static fn(callable $listener): bool => $listener !== $callback,
));
} }
/**
* Remove all listeners for a specific event.
*
* @param string $event the event name
*
* @return void
*/
public function removeAllListeners(string $event): void public function removeAllListeners(string $event): void
{ {
if (isset($this->listeners[$event]) === true) { unset($this->listeners[$event]);
unset($this->listeners[$event]);
}
} }
/**
* Remove the current singleton instance of the EventDispatcher.
*
* @return void
*/
public static function resetInstance(): void public static function resetInstance(): void
{ {
self::$instance = null; self::$instance = null;

@ -13,8 +13,8 @@ use Exception;
* instances with custom initialization parameters. It also performs * instances with custom initialization parameters. It also performs
* class autoloading. * class autoloading.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
*/ */
class Loader class Loader
{ {

@ -11,8 +11,8 @@ use flight\util\Collection;
* all the super globals $_GET, $_POST, $_COOKIE, and $_FILES * all the super globals $_GET, $_POST, $_COOKIE, and $_FILES
* are stored and accessible via the Request object. * are stored and accessible via the Request object.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
* *
* The default request properties are: * The default request properties are:
* *

@ -12,8 +12,8 @@ use flight\core\EventDispatcher;
* contains the response headers, HTTP status code, and response * contains the response headers, HTTP status code, and response
* body. * body.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
*/ */
class Response class Response
{ {

@ -9,8 +9,8 @@ namespace flight\net;
* an assigned callback function. The Router tries to match the * 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 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
*/ */
class Route class Route
{ {

@ -12,8 +12,8 @@ use flight\net\Route;
* an assigned callback function. The Router tries to match the * 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 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
*/ */
class Router class Router
{ {

@ -9,8 +9,8 @@ namespace flight\template;
* methods for managing view data and inserts the data into * methods for managing view data and inserts the data into
* view templates upon rendering. * view templates upon rendering.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @license https://docs.flightphp.com/license MIT
*/ */
class View class View
{ {

@ -13,10 +13,10 @@ use JsonSerializable;
* The Collection class allows you to access a set of data * The Collection class allows you to access a set of data
* using both array and object notation. * using both array and object notation.
* *
* @license MIT, http://flightphp.com/license * @copyright 2011 Mike Cao https://mikecao.com
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @implements ArrayAccess<string, mixed> * @implements ArrayAccess<string, mixed>
* @implements Iterator<string, mixed> * @implements Iterator<string, mixed>
* @license https://docs.flightphp.com/license MIT
*/ */
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
{ {

@ -23,11 +23,18 @@ class EngineTest extends TestCase
public function setUp(): void public function setUp(): void
{ {
$_SERVER = []; $_SERVER = [];
$_REQUEST = [];
$_GET = [];
$_POST = [];
// Static flag leaks across tests (and across Engine instances).
Request::$allowMethodOverride = true;
} }
public function tearDown(): void public function tearDown(): void
{ {
$_SERVER = []; $_SERVER = [];
$_REQUEST = [];
Request::$allowMethodOverride = true;
} }
public function testInitBeforeStart(): void public function testInitBeforeStart(): void
@ -1207,4 +1214,170 @@ class EngineTest extends TestCase
$this->expectExceptionMessage("/path/to/nowhere cannot be found."); $this->expectExceptionMessage("/path/to/nowhere cannot be found.");
$engine->download('/path/to/nowhere'); $engine->download('/path/to/nowhere');
} }
/**
* Regression for GHSA method-override opt-out: setting the flag to false must
* prevent X-HTTP-Method-Override from selecting a different route.
*
* The original mitigation assigned Request::$allowMethodOverride via
* $self->request()::$allowMethodOverride, which built Request (and cached the
* overridden verb) before the static was set.
*/
public function testAllowMethodOverrideFalseBlocksHeaderOverrideOnStart(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/test';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$engine = new Engine();
$engine->set('flight.allow_method_override', false);
$hit = null;
$engine->route('GET /test', function () use (&$hit) {
$hit = 'get';
echo 'get';
});
$engine->route('DELETE /test', function () use (&$hit) {
$hit = 'delete';
echo 'delete';
});
$this->expectOutputString('get');
$engine->start();
$this->assertSame('get', $hit);
$this->assertFalse(Request::$allowMethodOverride);
$this->assertSame('GET', $engine->request()->method);
}
public function testAllowMethodOverrideFalseBlocksPostMethodFieldOnStart(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REQUEST_URI'] = '/test';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_REQUEST['_method'] = 'PUT';
$engine = new Engine();
$engine->set('flight.allow_method_override', false);
$hit = null;
$engine->route('POST /test', function () use (&$hit) {
$hit = 'post';
echo 'post';
});
$engine->route('PUT /test', function () use (&$hit) {
$hit = 'put';
echo 'put';
});
$this->expectOutputString('post');
$engine->start();
$this->assertSame('post', $hit);
$this->assertSame('POST', $engine->request()->method);
}
public function testAllowMethodOverrideTrueStillHonorsHeaderOnStart(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/test';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$engine = new Engine();
// default is true; set explicitly for clarity
$engine->set('flight.allow_method_override', true);
$hit = null;
$engine->route('GET /test', function () use (&$hit) {
$hit = 'get';
echo 'get';
});
$engine->route('DELETE /test', function () use (&$hit) {
$hit = 'delete';
echo 'delete';
});
$this->expectOutputString('delete');
$engine->start();
$this->assertSame('delete', $hit);
$this->assertTrue(Request::$allowMethodOverride);
$this->assertSame('DELETE', $engine->request()->method);
}
/**
* App code often touches request() before start() (set url, inspect headers, etc.).
* The flag must still win even when Request was constructed early under the default.
*/
public function testAllowMethodOverrideFalseRefreshesMethodIfRequestBuiltEarly(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/test';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$engine = new Engine();
$engine->set('flight.allow_method_override', false);
// Construct Request while static is still the default (true)
$this->assertTrue(Request::$allowMethodOverride);
$early = $engine->request();
$this->assertSame('DELETE', $early->method, 'pre-start construction still sees default override=on');
$hit = null;
$engine->route('GET /test', function () use (&$hit) {
$hit = 'get';
echo 'get';
});
$engine->route('DELETE /test', function () use (&$hit) {
$hit = 'delete';
echo 'delete';
});
$this->expectOutputString('get');
$engine->start();
$this->assertSame('get', $hit);
$this->assertFalse(Request::$allowMethodOverride);
$this->assertSame('GET', $engine->request()->method);
$this->assertSame($early, $engine->request(), 'same Request instance is refreshed, not replaced');
}
public function testAllowMethodOverrideFalseDoesNotClobberManualMethodWithoutOverrideInput(): void
{
// No X-HTTP-Method-Override / _method — manual method assignment must survive start().
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/someRoute';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$engine = new Engine();
$engine->set('flight.allow_method_override', false);
$engine->route('GET /someRoute', function () {
echo 'i ran';
}, true);
$engine->request()->method = 'HEAD';
$engine->request()->url = '/someRoute';
$this->expectOutputString('');
$engine->start();
$this->assertSame('HEAD', $engine->request()->method);
}
} }

@ -31,11 +31,15 @@ class RequestTest extends TestCase
$_COOKIE = []; $_COOKIE = [];
$_FILES = []; $_FILES = [];
// Static flag leaks across tests; always restore the framework default.
Request::$allowMethodOverride = true;
$this->request = new Request(); $this->request = new Request();
} }
protected function tearDown(): void protected function tearDown(): void
{ {
Request::$allowMethodOverride = true;
unset($_REQUEST); unset($_REQUEST);
unset($_SERVER); unset($_SERVER);
} }
@ -127,6 +131,49 @@ class RequestTest extends TestCase
$this->assertEquals('PUT', $request->method); $this->assertEquals('PUT', $request->method);
} }
public function testMethodOverrideDisabledIgnoresHeader(): void
{
Request::$allowMethodOverride = false;
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE';
$request = new Request();
$this->assertSame('GET', $request->method);
$this->assertSame('GET', Request::getMethod());
}
public function testMethodOverrideDisabledIgnoresPostField(): void
{
Request::$allowMethodOverride = false;
$_SERVER['REQUEST_METHOD'] = 'POST';
$_REQUEST['_method'] = 'PUT';
$request = new Request();
$this->assertSame('POST', $request->method);
$this->assertSame('POST', Request::getMethod());
}
public function testMethodOverrideFlagMustBeSetBeforeConstruction(): void
{
// Documents the caching behavior: flipping the static after construct
// does not rewrite the already-cached $request->method.
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE';
Request::$allowMethodOverride = true;
$request = new Request();
$this->assertSame('DELETE', $request->method);
Request::$allowMethodOverride = false;
$this->assertSame('DELETE', $request->method, 'cached method is not auto-refreshed');
$this->assertSame('GET', Request::getMethod(), 'getMethod() respects the new flag');
$request->method = Request::getMethod();
$this->assertSame('GET', $request->method);
}
public function testHttps(): void public function testHttps(): void
{ {
$_SERVER['HTTPS'] = 'on'; $_SERVER['HTTPS'] = 'on';

Loading…
Cancel
Save