Merge pull request #559 from flightphp/container

Added Containerization to Core
pull/560/head v3.7.0
n0nag0n 10 months ago committed by GitHub
commit 8d772b51b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -41,9 +41,11 @@
},
"require-dev": {
"ext-pdo_sqlite": "*",
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.10",
"league/container": "^4.2",
"level-2/dice": "^4.0",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5",
"rregeer/phpunit-coverage-check": "^0.3.1",
"squizlabs/php_codesniffer": "^3.8"
},

@ -30,17 +30,17 @@ use flight\net\Route;
* @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response.
*
* # Routing
* @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a URL to a callback function with all applicable methods
* @method void group(string $pattern, callable $callback, array<int, callable|object> $group_middlewares = [])
* Groups a set of routes together under a common prefix.
* @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a POST URL to a callback function.
* @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a PUT URL to a callback function.
* @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a PATCH URL to a callback function.
* @method Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @method Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a DELETE URL to a callback function.
* @method Router router() Gets router
* @method string getUrl(string $alias) Gets a url from an alias
@ -138,6 +138,9 @@ class Engine
$this->dispatcher->reset();
}
// Add this class to Dispatcher
$this->dispatcher->setEngine($this);
// Register default components
$this->loader->register('request', Request::class);
$this->loader->register('response', Response::class);
@ -217,6 +220,18 @@ class Engine
$this->error($e);
}
/**
* Registers the container handler
*
* @param callable|object $containerHandler Callback function or PSR-11 Container object that sets the container and how it will inject classes
*
* @return void
*/
public function registerContainerHandler($containerHandler): void
{
$this->dispatcher->setContainerHandler($containerHandler);
}
/**
* Maps a callback to a framework method.
*
@ -605,11 +620,11 @@ class Engine
* Routes a URL to a callback function.
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @param callable|string $callback Callback function
* @param bool $pass_route Pass the matching route object to the callback
* @param string $alias The alias for the route
*/
public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function _route(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->router()->map($pattern, $callback, $pass_route, $alias);
}
@ -630,10 +645,10 @@ class Engine
* Routes a URL to a callback function.
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
*/
public function _post(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void
public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void
{
$this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias);
}
@ -642,10 +657,10 @@ class Engine
* Routes a URL to a callback function.
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
*/
public function _put(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void
public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void
{
$this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias);
}
@ -654,10 +669,10 @@ class Engine
* Routes a URL to a callback function.
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
*/
public function _patch(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void
public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void
{
$this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias);
}
@ -666,10 +681,10 @@ class Engine
* Routes a URL to a callback function.
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @param callable|string $callback Callback function or string class->method
* @param bool $pass_route Pass the matching route object to the callback
*/
public function _delete(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void
public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void
{
$this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias);
}

@ -19,24 +19,25 @@ require_once __DIR__ . '/autoload.php';
* @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)
* @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 registerContainerHandler(callable|object $containerHandler) Registers a container handler.
*
* # Routing
* @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '')
* @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 $callback, bool $pass_route = false, string $alias = '')
* @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 $callback, bool $pass_route = false, string $alias = '')
* @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 $callback, bool $pass_route = false, string $alias = '')
* @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 $callback, bool $pass_route = false, string $alias = '')
* @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
@ -101,34 +102,6 @@ class Flight
{
}
/**
* Registers a class to a framework method.
*
* # Usage example:
* ```
* Flight::register('user', User::class);
*
* Flight::user(); # <- Return a User instance
* ```
*
* @param string $name Static method name
* @param class-string<T> $class Fully Qualified Class Name
* @param array<int, mixed> $params Class constructor params
* @param ?Closure(T $instance): void $callback Perform actions with the instance
*
* @template T of object
*/
public static function register($name, $class, $params = [], $callback = null): void
{
static::__callStatic('register', [$name, $class, $params, $callback]);
}
/** Unregisters a class. */
public static function unregister(string $methodName): void
{
static::__callStatic('unregister', [$methodName]);
}
/**
* Handles calls to static methods.
*
@ -140,7 +113,7 @@ class Flight
*/
public static function __callStatic(string $name, array $params)
{
return Dispatcher::invokeMethod([self::app(), $name], $params);
return self::app()->{$name}(...$params);
}
/** @return Engine Application instance */

@ -6,8 +6,8 @@ namespace flight\core;
use Closure;
use Exception;
use flight\Engine;
use InvalidArgumentException;
use ReflectionClass;
use TypeError;
/**
@ -25,6 +25,12 @@ class Dispatcher
public const FILTER_AFTER = 'after';
private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER];
/** @var mixed $containerException Exception message if thrown by setting the container as a callable method */
protected $containerException = null;
/** @var ?Engine $engine Engine instance */
protected ?Engine $engine = null;
/** @var array<string, Closure(): (void|mixed)> Mapped events. */
protected array $events = [];
@ -35,6 +41,30 @@ class Dispatcher
*/
protected array $filters = [];
/**
* This is a container for the dependency injection.
*
* @var callable|object|null
*/
protected $containerHandler = null;
/**
* Sets the dependency injection container handler.
*
* @param callable|object $containerHandler Dependency injection container
*
* @return void
*/
public function setContainerHandler($containerHandler): void
{
$this->containerHandler = $containerHandler;
}
public function setEngine(Engine $engine): void
{
$this->engine = $engine;
}
/**
* Dispatches an event.
*
@ -80,10 +110,10 @@ class Dispatcher
$requestedMethod = $this->get($eventName);
if ($requestedMethod === null) {
throw new Exception("Event '$eventName' isn't found.");
throw new Exception("Event '{$eventName}' isn't found.");
}
return $requestedMethod(...$params);
return $this->execute($requestedMethod, $params);
}
/**
@ -194,7 +224,7 @@ class Dispatcher
*
* @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter.
*/
public static function filter(array $filters, array &$params, &$output): void
public function filter(array $filters, array &$params, &$output): void
{
foreach ($filters as $key => $callback) {
if (!is_callable($callback)) {
@ -219,22 +249,33 @@ class Dispatcher
* @return mixed Function results
* @throws Exception If `$callback` also throws an `Exception`.
*/
public static function execute($callback, array &$params = [])
public function execute($callback, array &$params = [])
{
$isInvalidFunctionName = (
is_string($callback)
&& !function_exists($callback)
);
if ($isInvalidFunctionName) {
throw new InvalidArgumentException('Invalid callback specified.');
if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) {
$callback = $this->parseStringClassAndMethod($callback);
}
if (is_array($callback)) {
return self::invokeMethod($callback, $params);
return $this->invokeCallable($callback, $params);
}
/**
* Parses a string into a class and method.
*
* @param string $classAndMethod Class and method
*
* @return array{class-string|object, string} Class and method
*/
public function parseStringClassAndMethod(string $classAndMethod): array
{
$class_parts = explode('->', $classAndMethod);
if (count($class_parts) === 1) {
$class_parts = explode('::', $class_parts[0]);
}
return self::callFunction($callback, $params);
$class = $class_parts[0];
$method = $class_parts[1];
return [ $class, $method ];
}
/**
@ -244,10 +285,11 @@ class Dispatcher
* @param array<int, mixed> &$params Function parameters
*
* @return mixed Function results
* @deprecated 3.7.0 Use invokeCallable instead
*/
public static function callFunction(callable $func, array &$params = [])
public function callFunction(callable $func, array &$params = [])
{
return call_user_func_array($func, $params);
return $this->invokeCallable($func, $params);
}
/**
@ -257,34 +299,172 @@ class Dispatcher
* @param array<int, mixed> &$params Class method parameters
*
* @return mixed Function results
* @throws TypeError For unexistent class name.
* @throws TypeError For nonexistent class name.
* @deprecated 3.7.0 Use invokeCallable instead
*/
public static function invokeMethod(array $func, array &$params = [])
public function invokeMethod(array $func, array &$params = [])
{
[$class, $method] = $func;
return $this->invokeCallable($func, $params);
}
if (is_string($class) && class_exists($class)) {
$constructor = (new ReflectionClass($class))->getConstructor();
$constructorParamsNumber = 0;
/**
* Invokes a callable (anonymous function or Class->method).
*
* @param array{class-string|object, string}|Callable $func Class method
* @param array<int, mixed> &$params Class method parameters
*
* @return mixed Function results
* @throws TypeError For nonexistent class name.
* @throws InvalidArgumentException If the constructor requires parameters
* @version 3.7.0
*/
public function invokeCallable($func, array &$params = [])
{
// If this is a directly callable function, call it
if (is_array($func) === false) {
$this->verifyValidFunction($func);
return call_user_func_array($func, $params);
}
if ($constructor !== null) {
$constructorParamsNumber = count($constructor->getParameters());
[$class, $method] = $func;
$resolvedClass = null;
// Only execute the container handler if it's not a Flight class
if (
$this->containerHandler !== null &&
(
(
is_object($class) === true &&
strpos(get_class($class), 'flight\\') === false
) ||
is_string($class) === true
)
) {
$containerHandler = $this->containerHandler;
$resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params);
if ($resolvedClass !== null) {
$class = $resolvedClass;
}
}
if ($constructorParamsNumber > 0) {
$exceptionMessage = "Method '$class::$method' cannot be called statically. ";
$exceptionMessage .= sprintf(
"$class::__construct require $constructorParamsNumber parameter%s",
$constructorParamsNumber > 1 ? 's' : ''
);
$this->verifyValidClassCallable($class, $method, $resolvedClass);
throw new InvalidArgumentException($exceptionMessage, E_ERROR);
}
// Class is a string, and method exists, create the object by hand and inject only the Engine
if (is_string($class) === true) {
$class = new $class($this->engine);
}
return call_user_func_array([ $class, $method ], $params);
}
/**
* Handles invalid callback types.
*
* @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback
* Callback function
*
* @throws InvalidArgumentException If `$callback` is an invalid type
*/
protected function verifyValidFunction($callback): void
{
$isInvalidFunctionName = (
is_string($callback)
&& !function_exists($callback)
);
if ($isInvalidFunctionName) {
throw new InvalidArgumentException('Invalid callback specified.');
}
}
/**
* Verifies if the provided class and method are valid callable.
*
* @param string|object $class The class name.
* @param string $method The method name.
* @param object|null $resolvedClass The resolved class.
*
* @throws Exception If the class or method is not found.
*
* @return void
*/
protected function verifyValidClassCallable($class, $method, $resolvedClass): void
{
$final_exception = null;
// Final check to make sure it's actually a class and a method, or throw an error
if (is_object($class) === false && class_exists($class) === false) {
$final_exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?");
// If this tried to resolve a class in a container and failed somehow, throw the exception
} elseif (isset($resolvedClass) === false && $this->containerException !== null) {
$final_exception = $this->containerException;
// Class is there, but no method
} elseif (is_object($class) === true && method_exists($class, $method) === false) {
$final_exception = new Exception("Class found, but method '" . get_class($class) . "::$method' not found.");
}
$class = new $class();
if ($final_exception !== null) {
$this->fixOutputBuffering();
throw $final_exception;
}
}
return call_user_func_array([$class, $method], $params);
/**
* Resolves the container class.
*
* @param callable|object $container_handler Dependency injection container
* @param class-string $class Class name
* @param array<int, mixed> &$params Class constructor parameters
*
* @return object Class object
*/
protected function resolveContainerClass($container_handler, $class, array &$params)
{
$class_object = null;
// PSR-11
if (
is_object($container_handler) === true &&
method_exists($container_handler, 'has') === true &&
$container_handler->has($class)
) {
$class_object = call_user_func([$container_handler, 'get'], $class);
// Just a callable where you configure the behavior (Dice, PHP-DI, etc.)
} elseif (is_callable($container_handler) === true) {
// This is to catch all the error that could be thrown by whatever container you are using
try {
$class_object = call_user_func($container_handler, $class, $params);
} catch (Exception $e) {
// could not resolve a class for some reason
$class_object = null;
// If the container throws an exception, we need to catch it
// and store it somewhere. If we just let it throw itself, it
// doesn't properly close the output buffers and can cause other
// issues.
// This is thrown in the verifyValidClassCallable method
$this->containerException = $e;
}
}
return $class_object;
}
/**
* Because this could throw an exception in the middle of an output buffer,
*
* @return void
*/
protected function fixOutputBuffering(): void
{
// Cause PHPUnit has 1 level of output buffering by default
if (ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) {
ob_end_clean();
}
}
/**

@ -81,11 +81,11 @@ class Route
* Constructor.
*
* @param string $pattern URL pattern
* @param callable $callback Callback function
* @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, callable $callback, array $methods, bool $pass, string $alias = '')
public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '')
{
$this->pattern = $pattern;
$this->callback = $callback;

@ -80,11 +80,11 @@ class Router
* Maps a URL pattern to a callback function.
*
* @param string $pattern URL pattern to match.
* @param callable $callback Callback function.
* @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, callable $callback, bool $pass_route = false, string $route_alias = ''): 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
@ -133,11 +133,11 @@ class Router
* Creates a GET based route
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @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, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function get(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('GET ' . $pattern, $callback, $pass_route, $alias);
}
@ -146,11 +146,11 @@ class Router
* Creates a POST based route
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @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, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function post(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('POST ' . $pattern, $callback, $pass_route, $alias);
}
@ -159,11 +159,11 @@ class Router
* Creates a PUT based route
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @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, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function put(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias);
}
@ -172,11 +172,11 @@ class Router
* Creates a PATCH based route
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @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, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function patch(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias);
}
@ -185,11 +185,11 @@ class Router
* Creates a DELETE based route
*
* @param string $pattern URL pattern to match
* @param callable $callback Callback function
* @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, callable $callback, bool $pass_route = false, string $alias = ''): Route
public function delete(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route
{
return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias);
}

@ -4,13 +4,16 @@ declare(strict_types=1);
namespace tests;
use ArgumentCountError;
use Closure;
use Exception;
use flight\core\Dispatcher;
use flight\Engine;
use InvalidArgumentException;
use PharIo\Manifest\InvalidEmailException;
use tests\classes\Hello;
use PHPUnit\Framework\TestCase;
use tests\classes\ContainerDefault;
use tests\classes\TesterClass;
use TypeError;
@ -107,11 +110,11 @@ class DispatcherTest extends TestCase
});
$this->dispatcher
->hook('hello', $this->dispatcher::FILTER_BEFORE, function (array &$params): void {
->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void {
// Manipulate the parameter
$params[0] = 'Fred';
})
->hook('hello', $this->dispatcher::FILTER_AFTER, function (array &$params, string &$output): void {
->hook('hello', Dispatcher::FILTER_AFTER, function (array &$params, string &$output): void {
// Manipulate the output
$output .= ' Have a nice day!';
});
@ -123,9 +126,10 @@ class DispatcherTest extends TestCase
public function testInvalidCallback(): void
{
$this->expectException(TypeError::class);
$this->expectException(Exception::class);
$this->expectExceptionMessage("Class 'NonExistentClass' not found. Is it being correctly autoloaded with Flight::path()?");
Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']);
$this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']);
}
public function testInvalidCallableString(): void
@ -133,27 +137,13 @@ class DispatcherTest extends TestCase
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid callback specified.');
Dispatcher::execute('inexistentGlobalFunction');
}
public function testInvalidCallbackBecauseConstructorParameters(): void
{
$class = TesterClass::class;
$method = 'instanceMethod';
$exceptionMessage = "Method '$class::$method' cannot be called statically. ";
$exceptionMessage .= "$class::__construct require 6 parameters";
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($exceptionMessage);
static $params = [];
Dispatcher::invokeMethod([$class, $method], $params);
$this->dispatcher->execute('nonexistentGlobalFunction');
}
// It will be useful for executing instance Controller methods statically
public function testCanExecuteAnNonStaticMethodStatically(): void
{
$this->assertSame('hello', Dispatcher::execute([Hello::class, 'sayHi']));
$this->assertSame('hello', $this->dispatcher->execute([Hello::class, 'sayHi']));
}
public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void
@ -191,7 +181,7 @@ class DispatcherTest extends TestCase
{
set_error_handler(function (int $errno, string $errstr): void {
$this->assertSame(E_USER_NOTICE, $errno);
$this->assertSame("Event 'myMethod' has been overriden!", $errstr);
$this->assertSame("Event 'myMethod' has been overridden!", $errstr);
});
$this->dispatcher->set('myMethod', function (): string {
@ -199,10 +189,10 @@ class DispatcherTest extends TestCase
});
$this->dispatcher->set('myMethod', function (): string {
return 'Overriden';
return 'Overridden';
});
$this->assertSame('Overriden', $this->dispatcher->run('myMethod'));
$this->assertSame('Overridden', $this->dispatcher->run('myMethod'));
restore_error_handler();
}
@ -218,7 +208,7 @@ class DispatcherTest extends TestCase
return 'Original';
})
->hook('myMethod', 'invalid', function (array &$params, &$output): void {
$output = 'Overriden';
$output = 'Overridden';
});
$this->assertSame('Original', $this->dispatcher->run('myMethod'));
@ -237,7 +227,7 @@ class DispatcherTest extends TestCase
$validCallable = function (): void {
};
Dispatcher::filter([$validCallable, $invalidCallable], $params, $output);
$this->dispatcher->filter([$validCallable, $invalidCallable], $params, $output);
}
public function testCallFunction6Params(): void
@ -247,8 +237,75 @@ class DispatcherTest extends TestCase
};
$params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6'];
$result = Dispatcher::callFunction($func, $params);
$result = $this->dispatcher->callFunction($func, $params);
$this->assertSame('helloparam1param2param3param4param5param6', $result);
}
public function testInvokeMethod(): void
{
$class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6');
$result = $this->dispatcher->invokeMethod([ $class, 'instanceMethod' ]);
$this->assertSame('param1', $class->param2);
}
public function testExecuteStringClassBadConstructParams(): void
{
$this->expectException(ArgumentCountError::class);
$this->expectExceptionMessageMatches('#Too few arguments to function tests\\\\classes\\\\TesterClass::__construct\(\), 1 passed .+ and exactly 6 expected#');
$this->dispatcher->execute(TesterClass::class . '->instanceMethod');
}
public function testExecuteStringClassNoConstruct(): void
{
$result = $this->dispatcher->execute(Hello::class . '->sayHi');
$this->assertSame('hello', $result);
}
public function testExecuteStringClassNoConstructDoubleColon(): void
{
$result = $this->dispatcher->execute(Hello::class . '::sayHi');
$this->assertSame('hello', $result);
}
public function testExecuteStringClassNoConstructArraySyntax(): void
{
$result = $this->dispatcher->execute([ Hello::class, 'sayHi' ]);
$this->assertSame('hello', $result);
}
public function testExecuteStringClassDefaultContainer(): void
{
$engine = new Engine();
$engine->set('test_me_out', 'You got it boss!');
$this->dispatcher->setEngine($engine);
$result = $this->dispatcher->execute(ContainerDefault::class . '->testTheContainer');
$this->assertSame('You got it boss!', $result);
}
public function testExecuteStringClassDefaultContainerDoubleColon(): void
{
$engine = new Engine();
$engine->set('test_me_out', 'You got it boss!');
$this->dispatcher->setEngine($engine);
$result = $this->dispatcher->execute(ContainerDefault::class . '::testTheContainer');
$this->assertSame('You got it boss!', $result);
}
public function testExecuteStringClassDefaultContainerArraySyntax(): void
{
$engine = new Engine();
$engine->set('test_me_out', 'You got it boss!');
$this->dispatcher->setEngine($engine);
$result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]);
$this->assertSame('You got it boss!', $result);
}
public function testExecuteStringClassDefaultContainerButForgotInjectingEngine(): void
{
$this->expectException(TypeError::class);
$this->expectExceptionMessageMatches('#Argument 1 passed to tests\\\\classes\\\\ContainerDefault::__construct\(\) must be an instance of flight\\\\Engine, null given#');
$result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]);
}
}

@ -5,12 +5,15 @@ declare(strict_types=1);
namespace tests;
use Exception;
use Flight;
use flight\database\PdoWrapper;
use flight\Engine;
use flight\net\Request;
use flight\net\Response;
use flight\util\Collection;
use PDOException;
use PHPUnit\Framework\TestCase;
use tests\classes\Container;
use tests\classes\ContainerDefault;
// phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore
class EngineTest extends TestCase
@ -676,4 +679,166 @@ class EngineTest extends TestCase
$engine->start();
$this->expectOutputString('before456before123OKafter123456after123');
}
public function testContainerDice() {
$engine = new Engine();
$dice = new \Dice\Dice();
$dice = $dice->addRules([
PdoWrapper::class => [
'shared' => true,
'constructParams' => [ 'sqlite::memory:' ]
]
]);
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', Container::class.'->testTheContainer');
$engine->request()->url = '/container';
$engine->start();
$this->expectOutputString('yay! I injected a collection, and it has 1 items');
}
public function testContainerDicePdoWrapperTest() {
$engine = new Engine();
$dice = new \Dice\Dice();
$dice = $dice->addRules([
PdoWrapper::class => [
'shared' => true,
'constructParams' => [ 'sqlite::memory:' ]
]
]);
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', Container::class.'->testThePdoWrapper');
$engine->request()->url = '/container';
$engine->start();
$this->expectOutputString('Yay! I injected a PdoWrapper, and it returned the number 5 from the database!');
}
public function testContainerDiceFlightEngine() {
$engine = new Engine();
$engine->set('test_me_out', 'You got it boss!');
$dice = new \Dice\Dice();
$dice = $dice->addRule('*', [
'substitutions' => [
Engine::class => $engine
]
]);
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', ContainerDefault::class.'->echoTheContainer');
$engine->request()->url = '/container';
$engine->start();
$this->expectOutputString('You got it boss!');
}
public function testContainerDicePdoWrapperTestBadParams() {
$engine = new Engine();
$dice = new \Dice\Dice();
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', Container::class.'->testThePdoWrapper');
$engine->request()->url = '/container';
$this->expectException(PDOException::class);
$this->expectExceptionMessage("invalid data source name");
$engine->start();
}
public function testContainerDiceBadClass() {
$engine = new Engine();
$dice = new \Dice\Dice();
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', 'BadClass->testTheContainer');
$engine->request()->url = '/container';
$this->expectException(Exception::class);
$this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?");
$engine->start();
}
public function testContainerDiceBadMethod() {
$engine = new Engine();
$dice = new \Dice\Dice();
$dice = $dice->addRules([
PdoWrapper::class => [
'shared' => true,
'constructParams' => [ 'sqlite::memory:' ]
]
]);
$engine->registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
$engine->route('/container', Container::class.'->badMethod');
$engine->request()->url = '/container';
$this->expectException(Exception::class);
$this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found.");
$engine->start();
}
public function testContainerPsr11() {
$engine = new Engine();
$container = new \League\Container\Container();
$container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class);
$container->add(Collection::class);
$container->add(PdoWrapper::class)->addArgument('sqlite::memory:');
$engine->registerContainerHandler($container);
$engine->route('/container', Container::class.'->testTheContainer');
$engine->request()->url = '/container';
$engine->start();
$this->expectOutputString('yay! I injected a collection, and it has 1 items');
}
public function testContainerPsr11ClassNotFound() {
$engine = new Engine();
$container = new \League\Container\Container();
$container->add(Container::class)->addArgument(Collection::class);
$container->add(Collection::class);
$engine->registerContainerHandler($container);
$engine->route('/container', 'BadClass->testTheContainer');
$engine->request()->url = '/container';
$this->expectException(Exception::class);
$this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?");
$engine->start();
}
public function testContainerPsr11MethodNotFound() {
$engine = new Engine();
$container = new \League\Container\Container();
$container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class);
$container->add(Collection::class);
$container->add(PdoWrapper::class)->addArgument('sqlite::memory:');
$engine->registerContainerHandler($container);
$engine->route('/container', Container::class.'->badMethod');
$engine->request()->url = '/container';
$this->expectException(Exception::class);
$this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found.");
$engine->start();
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace tests\classes;
use flight\database\PdoWrapper;
use flight\util\Collection;
class Container
{
protected Collection $collection;
protected PdoWrapper $pdoWrapper;
public function __construct(Collection $collection, PdoWrapper $pdoWrapper)
{
$this->collection = $collection;
$this->pdoWrapper = $pdoWrapper;
}
public function testTheContainer()
{
$this->collection->whatever = 'yay!';
echo 'yay! I injected a collection, and it has ' . $this->collection->count() . ' items';
}
public function testThePdoWrapper()
{
$value = intval($this->pdoWrapper->fetchField('SELECT 5'));
echo 'Yay! I injected a PdoWrapper, and it returned the number ' . $value . ' from the database!';
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace tests\classes;
use flight\Engine;
class ContainerDefault
{
protected Engine $app;
public function __construct(Engine $engine)
{
$this->app = $engine;
}
public function testTheContainer()
{
return $this->app->get('test_me_out');
}
public function echoTheContainer()
{
echo $this->app->get('test_me_out');
}
public function testUi()
{
echo '<span id="infotext">Route text:</span> The container successfully injected a value into the engine! Engine class: <b>' . get_class($this->app) . '</b> test_me_out Value: <b>' . $this->app->get('test_me_out') . '</b>';
}
}

@ -81,6 +81,8 @@ class LayoutMiddleware
<li><a href="/redirect/before%2Fafter">Slash in Param</a></li>
<li><a href="/わたしはひとです">UTF8 URL</a></li>
<li><a href="/わたしはひとです/ええ">UTF8 URL w/ Param</a></li>
<li><a href="/dice">Dice Container</a></li>
<li><a href="/no-container">No Container Registered</a></li>
</ul>
HTML;
echo '<div id="container">';

@ -2,6 +2,10 @@
declare(strict_types=1);
use flight\database\PdoWrapper;
use tests\classes\Container;
use tests\classes\ContainerDefault;
/*
* This is the test file where we can open up a quick test server and make
* sure that the UI is really working the way we would expect it to.
@ -139,6 +143,10 @@ Flight::group('', function () {
Flight::route('/redirect/@id', function ($id) {
echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:' . ($id === 'before/after' ? 'green' : 'red') . '; font-weight: bold;">' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . '</span>';
});
Flight::set('test_me_out', 'You got it boss!'); // used in /no-container route
Flight::route('/no-container', ContainerDefault::class . '->testUi');
Flight::route('/dice', Container::class . '->testThePdoWrapper');
}, [ new LayoutMiddleware() ]);
// Test 9: JSON output (should not output any other html)
@ -167,4 +175,23 @@ Flight::map('notFound', function () {
echo "<a href='/'>Go back</a>";
});
Flight::map('start', function () {
if (Flight::request()->url === '/dice') {
$dice = new \Dice\Dice();
$dice = $dice->addRules([
PdoWrapper::class => [
'shared' => true,
'constructParams' => [ 'sqlite::memory:' ]
]
]);
Flight::registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params);
});
}
// Default start behavior now
Flight::_start();
});
Flight::start();

Loading…
Cancel
Save