mirror of
https://github.com/marcostoll/FF.git
synced 2025-03-21 15:40:00 +01:00
[FEATURE] dispatching and controllers
This commit is contained in:
parent
b1183f11d2
commit
7421585697
74
src/Controllers/AbstractController.php
Normal file
74
src/Controllers/AbstractController.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of AbstractController
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Controllers;
|
||||
|
||||
use FF\Factories\SF;
|
||||
use FF\Services\Dispatching\Dispatcher;
|
||||
use FF\Services\Templating\TemplateRendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Class AbstractController
|
||||
*
|
||||
* Concrete sub classes should define one or more action methods.
|
||||
*
|
||||
* Any action method must meet the following requirements:
|
||||
* - must be public
|
||||
* - must not be static
|
||||
* - must return an instance of Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* Action methods may define any number of arguments
|
||||
*
|
||||
* @package FF\Dispatching
|
||||
*/
|
||||
abstract class AbstractController
|
||||
{
|
||||
/**
|
||||
* For use with the BaseNamespaceClassLocator of the ControllersFactory
|
||||
*/
|
||||
const COMMON_NS_SUFFIX = 'Controllers';
|
||||
|
||||
/**
|
||||
* Forwards to another controller action
|
||||
*
|
||||
* This method can be invoked with an arbitrary amount of arguments.
|
||||
* Any $args will be passed to the designated forwarded action in the given order.
|
||||
*
|
||||
* @param AbstractController|string $controller A controller instance or the class identifier of a controller class
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
* @return Response
|
||||
*/
|
||||
protected function forward($controller, string $action, ...$args)
|
||||
{
|
||||
/** @var Dispatcher $dispatcher */
|
||||
$dispatcher = SF::i()->get('dispatching/dispatcher');
|
||||
|
||||
return $dispatcher->forward($controller, $action, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a template
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
protected function render(string $template, array $data = []): string
|
||||
{
|
||||
return $this->getTemplateRenderer()->render($template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateRendererInterface
|
||||
*/
|
||||
protected abstract function getTemplateRenderer(): TemplateRendererInterface;
|
||||
}
|
89
src/Events/Dispatching/PostDispatch.php
Normal file
89
src/Events/Dispatching/PostDispatch.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PostDispatch
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Dispatching;
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Events\AbstractEvent;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Class PostDispatch
|
||||
*
|
||||
* @package FF\Events\Dispatching
|
||||
*/
|
||||
class PostDispatch extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* @var AbstractController
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $args;
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param AbstractController $controller
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
*/
|
||||
public function __construct(Response $response, AbstractController $controller, $action, array $args = [])
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->controller = $controller;
|
||||
$this->action = $action;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function getResponse(): Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractController
|
||||
*/
|
||||
public function getController(): AbstractController
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getArgs(): array
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
}
|
91
src/Events/Dispatching/PostRoute.php
Normal file
91
src/Events/Dispatching/PostRoute.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PostRoute
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Dispatching;
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Events\AbstractEvent;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class PostRoute
|
||||
*
|
||||
* @package FF\Events\Dispatching
|
||||
*/
|
||||
class PostRoute extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var AbstractController
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $args = [];
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param AbstractController $controller
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
*/
|
||||
public function __construct(Request $request, AbstractController $controller, $action, array $args = [])
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->controller = $controller;
|
||||
$this->action = $action;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractController
|
||||
*/
|
||||
public function getController(): AbstractController
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the action arguments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArgs(): array
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
}
|
45
src/Events/Dispatching/PreDispatch.php
Normal file
45
src/Events/Dispatching/PreDispatch.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PreDispatch
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Dispatching;
|
||||
|
||||
use FF\Events\AbstractEvent;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class PreDispatch
|
||||
*
|
||||
* @package FF\Events\Dispatching
|
||||
*/
|
||||
class PreDispatch extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the request
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
73
src/Events/Dispatching/PreForward.php
Normal file
73
src/Events/Dispatching/PreForward.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of PreForward
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Events\Dispatching;
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Events\AbstractEvent;
|
||||
|
||||
/**
|
||||
* Class PreForward
|
||||
*
|
||||
* @package FF\Events\Dispatching
|
||||
*/
|
||||
class PreForward extends AbstractEvent
|
||||
{
|
||||
/**
|
||||
* @var AbstractController
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $args = [];
|
||||
|
||||
/**
|
||||
* @param AbstractController $controller
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
*/
|
||||
public function __construct(AbstractController $controller, string $action, array $args = [])
|
||||
{
|
||||
$this->controller = $controller;
|
||||
$this->action = $action;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractController
|
||||
*/
|
||||
public function getController(): AbstractController
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getArgs(): array
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
}
|
81
src/Factories/ControllersFactory.php
Normal file
81
src/Factories/ControllersFactory.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of ControllersFactory
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Factories;
|
||||
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Factories\ClassLocators\BaseNamespaceClassLocator;
|
||||
use FF\Factories\ClassLocators\ClassLocatorInterface;
|
||||
|
||||
/**
|
||||
* Class ControllersFactory
|
||||
*
|
||||
* @package FF\Factories
|
||||
*/
|
||||
class ControllersFactory extends AbstractSingletonFactory
|
||||
{
|
||||
/**
|
||||
* @var ControllersFactory
|
||||
*/
|
||||
protected static $instance;
|
||||
|
||||
/**
|
||||
* Declared protected to prevent external usage.
|
||||
* Uses a BaseNamespaceClassLocator pre-configured with the 'Events' as common suffix and the FF namespace.
|
||||
* @see \FF\Factories\ClassLocators\BaseNamespaceClassLocator
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
parent::__construct(new BaseNamespaceClassLocator(AbstractController::COMMON_NS_SUFFIX, 'FF'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declared protected to prevent external usage
|
||||
*/
|
||||
protected function __clone()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @return BaseNamespaceClassLocator
|
||||
*/
|
||||
public function getClassLocator(): ClassLocatorInterface
|
||||
{
|
||||
return parent::getClassLocator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the singleton instance of this class
|
||||
*
|
||||
* @return ControllersFactory
|
||||
*/
|
||||
public static function getInstance(): ControllersFactory
|
||||
{
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new ControllersFactory();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @return AbstractController
|
||||
*/
|
||||
public function create(string $classIdentifier, ...$args)
|
||||
{
|
||||
/** @var AbstractController $controller */
|
||||
$controller = parent::create($classIdentifier, ...$args);
|
||||
return $controller;
|
||||
}
|
||||
}
|
344
src/Services/Dispatching/Dispatcher.php
Normal file
344
src/Services/Dispatching/Dispatcher.php
Normal file
@ -0,0 +1,344 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of Dispatcher
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Dispatching;
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Factories\ControllersFactory;
|
||||
use FF\Factories\Exceptions\ClassNotFoundException;
|
||||
use FF\Services\AbstractService;
|
||||
use FF\Services\Dispatching\Exceptions\ControllerInspectionException;
|
||||
use FF\Services\Dispatching\Exceptions\IncompleteRouteException;
|
||||
use FF\Services\Exceptions\ResourceNotFoundException;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException as SymfonyResourceNotFoundException;
|
||||
use Symfony\Component\Routing\Loader\YamlFileLoader;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Class Dispatcher
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - routing-yaml : string - path to routing yaml
|
||||
* - fire-events : bool (default: false) - whether to fire rendering events
|
||||
*
|
||||
* @package FF\Services\Dispatching
|
||||
*/
|
||||
class Dispatcher extends AbstractService
|
||||
{
|
||||
const RESERVED_ROUTE_PARAMS = ['controller', 'action', '_route'];
|
||||
|
||||
/**
|
||||
* @var RouteCollection
|
||||
*/
|
||||
protected $routes;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $fireEvents;
|
||||
|
||||
/**
|
||||
* @return RouteCollection
|
||||
*/
|
||||
public function getRoutes(): RouteCollection
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RouteCollection $routes
|
||||
* @return $this
|
||||
*/
|
||||
public function setRoutes(RouteCollection $routes)
|
||||
{
|
||||
$this->routes = $routes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getFireEvents(): bool
|
||||
{
|
||||
return $this->fireEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $fireEvents
|
||||
* @return $this
|
||||
*/
|
||||
public function setFireEvents(bool $fireEvents)
|
||||
{
|
||||
$this->fireEvents = $fireEvents;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a route's path by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function getRoutePath(string $name): string
|
||||
{
|
||||
$route = $this->routes->get($name);
|
||||
if (is_null($route)) return '';
|
||||
|
||||
$path = $route->getPath();
|
||||
return !empty($path) ? $path : '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a relative path
|
||||
*
|
||||
* Strips non-filled path tokens from the end of the path.
|
||||
* Returns an empty string if $routeName is not found.
|
||||
*
|
||||
* @param string $routeName
|
||||
* @param array $namedArgs
|
||||
* @return string
|
||||
*/
|
||||
public function buildPath(string $routeName, array $namedArgs = []): string
|
||||
{
|
||||
$route = $this->routes->get($routeName);
|
||||
if (is_null($route)) return '';
|
||||
|
||||
// add omitted args having defaults in route's definition
|
||||
foreach ($route->getDefaults() as $name => $default) {
|
||||
if ($name == 'controller' || $name == 'action') continue;
|
||||
if (array_key_exists($name, $namedArgs)) continue;
|
||||
|
||||
$namedArgs[$name] = $default;
|
||||
}
|
||||
|
||||
// fill-in args in route's path
|
||||
$path = $route->getPath();
|
||||
foreach ($namedArgs as $key => $value) {
|
||||
$path = str_replace('{' . $key . '}', $value, $path);
|
||||
}
|
||||
|
||||
// strip unfilled args from end of path
|
||||
// e.g. /something/foo/{bar}
|
||||
$path = preg_replace('~(/{[^}]+})+$~', '', $path);
|
||||
|
||||
if (empty($path)) $path = '/';
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves parameters of a matching route for the request given
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array|null
|
||||
* @throws IncompleteRouteException
|
||||
*/
|
||||
public function match(Request $request): ?array
|
||||
{
|
||||
$context = new RequestContext();
|
||||
$context->fromRequest($request);
|
||||
|
||||
try {
|
||||
$matcher = new UrlMatcher($this->routes, $context);
|
||||
$pathInfo = $context->getPathInfo();
|
||||
$parameters = $matcher->match($pathInfo);
|
||||
if (!isset($parameters['controller'])) {
|
||||
throw new IncompleteRouteException(
|
||||
'controller param missing from route [' . $parameters['_route'] . ']'
|
||||
);
|
||||
}
|
||||
if (!isset($parameters['action'])) {
|
||||
throw new IncompleteRouteException('action param missing from route [' . $parameters['_route'] . ']');
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
} catch (SymfonyResourceNotFoundException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a request
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws ResourceNotFoundException no route found for request
|
||||
* @fires Dispatching\PreDispatch
|
||||
* @fires Dispatching\PostRoute
|
||||
* @fires Dispatching\PostDispatch
|
||||
*/
|
||||
public function dispatch(Request $request): Response
|
||||
{
|
||||
if ($this->fireEvents) {
|
||||
$this->fire('Dispatching\PreDispatch', $request);
|
||||
}
|
||||
|
||||
$parameters = $this->match($request);
|
||||
if (is_null($parameters)) {
|
||||
throw new ResourceNotFoundException('no route found for request [' . $request->getPathInfo() . ']');
|
||||
}
|
||||
|
||||
try {
|
||||
$controller = ControllersFactory::getInstance()->create($parameters['controller']);
|
||||
$action = $parameters['action'];
|
||||
$args = $this->extractArgs($parameters);
|
||||
$actionArgs = $this->buildActionArgs($controller, $action, $args);
|
||||
} catch (ClassNotFoundException $e) {
|
||||
throw new ResourceNotFoundException('controller [' . $parameters['controller'] . '] not found', 0, $e);
|
||||
} catch(ControllerInspectionException $e) {
|
||||
throw new ResourceNotFoundException(
|
||||
'action [' . $parameters['action'] . '] not found in controller [' . $parameters['controller'] . ']',
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->fireEvents) {
|
||||
$this->fire('Dispatching\PostRoute', $request, $controller, $action, $args);
|
||||
}
|
||||
|
||||
/** @var Response $response */
|
||||
$response = call_user_func_array([$controller, $action], $actionArgs);
|
||||
|
||||
if ($this->fireEvents) {
|
||||
$this->fire('Dispatching\PostDispatch', $response, $controller, $action, $actionArgs);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards to another controller action
|
||||
*
|
||||
* This method can be invoked with an arbitrary amount of arguments.
|
||||
* Any $args will be passed to the designated forwarded action in the given order.
|
||||
*
|
||||
* @param AbstractController|string $controller A controller instance or the class identifier of a controller class
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException action not callable
|
||||
* @fires Dispatching\PreForward
|
||||
*/
|
||||
public function forward($controller, string $action, ...$args)
|
||||
{
|
||||
if (is_string($controller)) {
|
||||
$controller = ControllersFactory::getInstance()->create($controller);
|
||||
}
|
||||
|
||||
if ($this->fireEvents) {
|
||||
$this->fire('Dispatching\PreForward', $controller, $action, $args);
|
||||
}
|
||||
|
||||
if (!method_exists($controller, $action) || !is_callable([$controller, $action])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'controller [' . get_class($controller) . '] does not define a callable action [' . $action . ']'
|
||||
);
|
||||
}
|
||||
|
||||
// invoke forwarded action
|
||||
$methodArgs = $this->buildActionArgs($controller, $action, $args);
|
||||
/** @var Response $response */
|
||||
$response = call_user_func_array([$controller, $action], $methodArgs);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the action arguments
|
||||
*
|
||||
* @param array $parameters
|
||||
* @return array
|
||||
*/
|
||||
protected function extractArgs(array $parameters): array
|
||||
{
|
||||
$args = [];
|
||||
foreach ($parameters as $key => $value) {
|
||||
if (in_array($key, self::RESERVED_ROUTE_PARAMS)) continue;
|
||||
$args[$key] = $value;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the arguments list for invoking the desired action
|
||||
*
|
||||
* @param AbstractController $controller
|
||||
* @param string $action
|
||||
* @param array $args
|
||||
* @return array
|
||||
* @throws ResourceNotFoundException
|
||||
* @throws ControllerInspectionException
|
||||
*/
|
||||
protected function buildActionArgs(AbstractController $controller, string $action, array $args): array
|
||||
{
|
||||
$methodArgs = [];
|
||||
try {
|
||||
$reflection = new \ReflectionMethod($controller, $action);
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
$name = $param->getName();
|
||||
if (!isset($args[$name])) {
|
||||
if (!$param->isOptional()) {
|
||||
throw new ResourceNotFoundException(
|
||||
'missing required argument [' . $name . '] for action [' . $action . '] '
|
||||
. 'of controller [' . get_class($controller) . ']'
|
||||
);
|
||||
}
|
||||
$methodArgs[] = $param->getDefaultValue();
|
||||
} else {
|
||||
$methodArgs[] = $args[$name];
|
||||
}
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new ControllerInspectionException(
|
||||
'error while inspecting action [' . $action . '] of controller [' . get_class($controller) . ']',
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
return $methodArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function initialize(array $options)
|
||||
{
|
||||
parent::initialize($options);
|
||||
|
||||
$this->fireEvents = $this->getOption('fire-events', false);
|
||||
|
||||
$locator = new FileLocator(dirname($this->getOption('routing-yaml')));
|
||||
$loader = new YamlFileLoader($locator);
|
||||
$this->routes = $loader->load(basename($this->getOption('routing-yaml')));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function validateOptions(array $options, array &$errors): bool
|
||||
{
|
||||
if (!isset($options['routing-yaml']) || empty($options['routing-yaml'])) {
|
||||
$errors[] = 'missing or empty mandatory option [routing-yaml]';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of ControllerInspectionException
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Dispatching\Exceptions;
|
||||
|
||||
/**
|
||||
* Class ControllerInspectionException
|
||||
*
|
||||
* @package FF\Services\Dispatching\Exceptions
|
||||
*/
|
||||
class ControllerInspectionException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of IncompleteRouteException
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Dispatching\Exceptions;
|
||||
|
||||
/**
|
||||
* Class IncompleteRouteException
|
||||
*
|
||||
* @package FF\Services\Dispatching\Exceptions
|
||||
*/
|
||||
class IncompleteRouteException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
21
src/Services/Exceptions/ResourceNotFoundException.php
Normal file
21
src/Services/Exceptions/ResourceNotFoundException.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of ResourceNotFoundException
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Services\Exceptions;
|
||||
|
||||
/**
|
||||
* Class ResourceNotFoundException
|
||||
*
|
||||
* @package FF\Services\Exceptions
|
||||
*/
|
||||
class ResourceNotFoundException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
41
tests/Factories/ControllersFactoryTest.php
Normal file
41
tests/Factories/ControllersFactoryTest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of ControllersFactoryTest
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Tests\Factories;
|
||||
|
||||
use FF\Factories\ClassLocators\BaseNamespaceClassLocator;
|
||||
use FF\Factories\ControllersFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Test ControllersFactoryTest
|
||||
*
|
||||
* @package FF\Tests
|
||||
*/
|
||||
class ControllersFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testGetInstance()
|
||||
{
|
||||
$instance = ControllersFactory::getInstance();
|
||||
$this->assertInstanceOf(ControllersFactory::class, $instance);
|
||||
$this->assertSame($instance, ControllersFactory::getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testGetClassLocator()
|
||||
{
|
||||
$this->assertInstanceOf(BaseNamespaceClassLocator::class, ControllersFactory::getInstance()->getClassLocator());
|
||||
}
|
||||
}
|
396
tests/Services/Dispatching/DispatcherTest.php
Normal file
396
tests/Services/Dispatching/DispatcherTest.php
Normal file
@ -0,0 +1,396 @@
|
||||
<?php
|
||||
/**
|
||||
* Definition of DispatcherTest
|
||||
*
|
||||
* @author Marco Stoll <marco@fast-forward-encoding.de>
|
||||
* @copyright 2019-forever Marco Stoll
|
||||
* @filesource
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Tests\Services\Dispatching {
|
||||
|
||||
use FF\Events\AbstractEvent;
|
||||
use FF\Events\Dispatching\PostDispatch;
|
||||
use FF\Events\Dispatching\PostRoute;
|
||||
use FF\Events\Dispatching\PreDispatch;
|
||||
use FF\Events\Dispatching\PreForward;
|
||||
use FF\Factories\ControllersFactory;
|
||||
use FF\Factories\Exceptions\ClassNotFoundException;
|
||||
use FF\Factories\ServicesFactory;
|
||||
use FF\Factories\SF;
|
||||
use FF\Services\Dispatching\Dispatcher;
|
||||
use FF\Services\Dispatching\Exceptions\IncompleteRouteException;
|
||||
use FF\Services\Events\EventBroker;
|
||||
use FF\Services\Exceptions\ConfigurationException;
|
||||
use FF\Services\Exceptions\ResourceNotFoundException;
|
||||
use FF\Tests\Services\Dispatching\Controllers\HelloWorldController;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Test DispatcherTest
|
||||
*
|
||||
* @package FF\Tests
|
||||
*/
|
||||
class DispatcherTest extends TestCase
|
||||
{
|
||||
const DEFAULT_OPTIONS = [
|
||||
'routing-yaml' => __DIR__ . '/routing/test-routing.yml'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var AbstractEvent[]
|
||||
*/
|
||||
protected static $lastEvents;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $uut;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
SF::setInstance(new ServicesFactory());
|
||||
|
||||
ControllersFactory::getInstance()
|
||||
->getClassLocator()
|
||||
->prependNamespaces(__NAMESPACE__);
|
||||
|
||||
/** @var EventBroker $eventBroker */
|
||||
$eventBroker = SF::i()->get('Events\EventBroker');
|
||||
|
||||
// register test listener
|
||||
$eventBroker
|
||||
->subscribe([__CLASS__, 'listener'], 'Dispatching\PreDispatch')
|
||||
->subscribe([__CLASS__, 'listener'], 'Dispatching\PostRoute')
|
||||
->subscribe([__CLASS__, 'listener'], 'Dispatching\PostDispatch')
|
||||
->subscribe([__CLASS__, 'listener'], 'Dispatching\PreForward');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->uut = new Dispatcher(self::DEFAULT_OPTIONS);
|
||||
|
||||
self::$lastEvents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy event listener
|
||||
*
|
||||
* @param AbstractEvent $event
|
||||
*/
|
||||
public static function listener(AbstractEvent $event)
|
||||
{
|
||||
self::$lastEvents[get_class($event)] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a request instance
|
||||
*
|
||||
* @param string $requestUri
|
||||
* @return Request
|
||||
*/
|
||||
protected function buildRequest($requestUri)
|
||||
{
|
||||
return new Request([], [], [], [], [], ['REQUEST_URI' => $requestUri]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testInitializeErrorMandatoryOptions()
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
|
||||
new Dispatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testSetGetRoutes()
|
||||
{
|
||||
$value = new RouteCollection();
|
||||
$same = $this->uut->setRoutes($value);
|
||||
$this->assertSame($this->uut, $same);
|
||||
$this->assertSame($value, $this->uut->getRoutes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testGetRoutePath()
|
||||
{
|
||||
$this->assertEquals('/default', $this->uut->getRoutePath('default'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testGetRoutePathMissing()
|
||||
{
|
||||
$this->assertEquals('', $this->uut->getRoutePath('unknown'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrl()
|
||||
{
|
||||
$path = $this->uut->buildPath('default');
|
||||
$this->assertEquals('/default', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrlExtraArgs()
|
||||
{
|
||||
$path = $this->uut->buildPath('default', ['foo' => 'bar']);
|
||||
$this->assertEquals('/default', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrlWithArgs()
|
||||
{
|
||||
$path = $this->uut->buildPath('with-args', ['foo' => 'foo', 'bar' => 'bar']);
|
||||
$this->assertEquals('/with-args/foo/bar', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrlWithoutArgs()
|
||||
{
|
||||
$path = $this->uut->buildPath('with-args');
|
||||
$this->assertEquals('/with-args', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrlDefaultArgs()
|
||||
{
|
||||
$path = $this->uut->buildPath('omitted-args', ['foo' => 'foo']);
|
||||
$this->assertEquals('/omitted-args/foo/bar', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testBuildUrlMissingArgs()
|
||||
{
|
||||
$path = $this->uut->buildPath('omitted-args');
|
||||
$this->assertEquals('/omitted-args/{foo}/bar', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchDefault()
|
||||
{
|
||||
$response = $this->uut->dispatch($this->buildRequest('/default'));
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals('default', $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchWithArgs()
|
||||
{
|
||||
$response = $this->uut->dispatch($this->buildRequest('/with-args/foo/bar'));
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals('foo-bar', $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchOmittedArgs()
|
||||
{
|
||||
$response = $this->uut->dispatch($this->buildRequest('/omitted-args/foo'));
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals('foo-bar', $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchEvents()
|
||||
{
|
||||
$this->uut->setFireEvents(true)
|
||||
->dispatch($this->buildRequest('/default'));
|
||||
|
||||
$this->assertArrayHasKey(PreDispatch::class, self::$lastEvents);
|
||||
$this->assertArrayHasKey(PostRoute::class, self::$lastEvents);
|
||||
$this->assertArrayHasKey(PostDispatch::class, self::$lastEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorRouteController()
|
||||
{
|
||||
$this->expectException(IncompleteRouteException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/missing-controller'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorRouteAction()
|
||||
{
|
||||
$this->expectException(IncompleteRouteException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/missing-action'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorNoRoute()
|
||||
{
|
||||
$this->expectException(ResourceNotFoundException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/unknown-path'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorNoController()
|
||||
{
|
||||
$this->expectException(ResourceNotFoundException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/unknown-controller'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorNoAction()
|
||||
{
|
||||
$this->expectException(ResourceNotFoundException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/unknown-action'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testDispatchErrorMissingArg()
|
||||
{
|
||||
$this->expectException(ResourceNotFoundException::class);
|
||||
|
||||
$this->uut->dispatch($this->buildRequest('/missing-arg'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardByObject()
|
||||
{
|
||||
$response = $this->uut->forward(new HelloWorldController(), 'default');
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($response->getContent(), 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardByClassName()
|
||||
{
|
||||
$response = $this->uut->forward('HelloWorldController', 'default');
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($response->getContent(), 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardEvents()
|
||||
{
|
||||
$this->uut->setFireEvents(true)
|
||||
->forward(new HelloWorldController(), 'default');
|
||||
|
||||
$this->assertArrayHasKey(PreForward::class, self::$lastEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardErrorController()
|
||||
{
|
||||
$this->expectException(ClassNotFoundException::class);
|
||||
|
||||
$this->uut->forward('UnknownController', 'foo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardErrorAction()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->uut->forward('HelloWorldController', 'unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the namesake method/feature
|
||||
*/
|
||||
public function testForwardErrorRequiredArg()
|
||||
{
|
||||
$this->expectException(ResourceNotFoundException::class);
|
||||
|
||||
$this->uut->forward('HelloWorldController', 'helloWorld');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace FF\Tests\Services\Dispatching\Controllers {
|
||||
|
||||
use FF\Controllers\AbstractController;
|
||||
use FF\Services\Templating\TemplateRendererInterface;
|
||||
use FF\Services\Templating\TwigRenderer;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HelloWorldController extends AbstractController
|
||||
{
|
||||
public function default(): Response
|
||||
{
|
||||
return new Response('default');
|
||||
}
|
||||
|
||||
public function helloWorld(string $foo, string $bar = 'baz'): Response
|
||||
{
|
||||
return new Response($foo . '-' . $bar);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateRendererInterface
|
||||
*/
|
||||
protected function getTemplateRenderer(): TemplateRendererInterface
|
||||
{
|
||||
return new TwigRenderer(['template-dir' => 'foo']);
|
||||
}
|
||||
}
|
||||
}
|
32
tests/Services/Dispatching/routing/test-routing.yml
Normal file
32
tests/Services/Dispatching/routing/test-routing.yml
Normal file
@ -0,0 +1,32 @@
|
||||
default:
|
||||
path: /default
|
||||
defaults: { controller: 'HelloWorldController', action: 'default' }
|
||||
|
||||
with-args:
|
||||
path: /with-args/{foo}/{bar}
|
||||
defaults: { controller: 'HelloWorldController', action: 'helloWorld'}
|
||||
|
||||
omitted-args:
|
||||
path: /omitted-args/{foo}/{bar}
|
||||
defaults: { controller: 'HelloWorldController', action: 'helloWorld', bar: bar }
|
||||
|
||||
# errors
|
||||
missing-controller:
|
||||
path: /missing-controller
|
||||
defaults: { action: 'index' }
|
||||
|
||||
missing-action:
|
||||
path: /missing-action
|
||||
defaults: { controller: 'HelloWorldController' }
|
||||
|
||||
unknown-controller:
|
||||
path: /unknown-controller
|
||||
defaults: { controller: 'UnknownController', action: 'index' }
|
||||
|
||||
unknown-action:
|
||||
path: /unknown-action
|
||||
defaults: { controller: 'HelloWorldController', action: 'unknown' }
|
||||
|
||||
missing-arg:
|
||||
path: /missing-arg
|
||||
defaults: { controller: 'HelloWorldController', action: 'helloWorld' }
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FF\Tests\Runtime;
|
||||
namespace FF\Tests\Services\Runtime;
|
||||
|
||||
use FF\Events\Runtime\Exception;
|
||||
use FF\Factories\ServicesFactory;
|
||||
|
Loading…
x
Reference in New Issue
Block a user