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,10 +22,7 @@ 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!';
@ -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