From 0d867c7371cc14de04a969f2885a4d518025a913 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 1 Feb 2019 00:55:49 +0100 Subject: [PATCH 01/16] Created MVCR Framework Implemented MVCR Framework --- .gitattributes | 4 + .gitignore | 6 + LICENSE | 21 + composer.json | 29 ++ src/Config/config.routes.php | 41 ++ src/Config/config.routing.php | 43 ++ src/FuzeWorks/Controller.php | 49 +++ src/FuzeWorks/Controllers.php | 172 ++++++++ src/FuzeWorks/Event/ControllerGetEvent.php | 86 ++++ src/FuzeWorks/Event/ModelGetEvent.php | 87 ++++ .../RouterLoadViewAndControllerEvent.php | 113 +++++ src/FuzeWorks/Event/ViewGetEvent.php | 101 +++++ .../Exception/ControllerException.php | 41 ++ src/FuzeWorks/Exception/HaltException.php | 41 ++ src/FuzeWorks/Exception/ModelException.php | 41 ++ src/FuzeWorks/Exception/NotFoundException.php | 41 ++ src/FuzeWorks/Exception/RouterException.php | 41 ++ src/FuzeWorks/Exception/ViewException.php | 41 ++ src/FuzeWorks/MVCRComponent.php | 71 ++++ src/FuzeWorks/Model.php | 50 +++ src/FuzeWorks/Models.php | 171 ++++++++ src/FuzeWorks/Router.php | 400 ++++++++++++++++++ src/FuzeWorks/View.php | 68 +++ src/FuzeWorks/Views.php | 179 ++++++++ test/autoload.php | 52 +++ test/bootstrap.php | 58 +++ ...ler.testdifferentcomponentpathpriority.php | 44 ++ ...ler.testdifferentcomponentpathpriority.php | 44 ++ .../controller.testgetsubdirectory.php | 44 ++ .../controller.controllerinvalidinstance.php | 42 ++ ...roller.testcontrollergeteventintervene.php | 44 ++ .../controller.testdefaultcallable.php | 44 ++ .../controller.testdefaultcallablehalt.php | 44 ++ ...troller.testdefaultcallablemissingview.php | 44 ++ .../controller.testgetcontroller.php | 43 ++ test/mcr/ControllersTest.php | 246 +++++++++++ test/mcr/MVCRTestAbstract.php | 60 +++ test/mcr/ModelsTest.php | 246 +++++++++++ test/mcr/RouterTest.php | 319 ++++++++++++++ test/mcr/ViewsTest.php | 255 +++++++++++ ...del.testdifferentcomponentpathpriority.php | 44 ++ ...del.testdifferentcomponentpathpriority.php | 44 ++ .../model.testgetsubdirectory.php | 44 ++ test/models/model.modelinvalidinstance.php | 42 ++ test/models/model.testgetmodel.php | 44 ++ .../model.testmodelgeteventintervene.php | 44 ++ test/phpunit.xml | 34 ++ ...ard.testdifferentcomponentpathpriority.php | 44 ++ ...ard.testdifferentcomponentpathpriority.php | 44 ++ .../view.standard.testgetsubdirectory.php | 44 ++ ...ew.othertype.testviewgeteventintervene.php | 44 ++ test/views/view.standard.testgetview.php | 44 ++ .../view.standard.viewinvalidinstance.php | 42 ++ test/views/view.test.testdefaultcallable.php | 49 +++ .../view.test.testdefaultcallablehalt.php | 45 ++ 55 files changed, 4218 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 src/Config/config.routes.php create mode 100644 src/Config/config.routing.php create mode 100644 src/FuzeWorks/Controller.php create mode 100644 src/FuzeWorks/Controllers.php create mode 100644 src/FuzeWorks/Event/ControllerGetEvent.php create mode 100644 src/FuzeWorks/Event/ModelGetEvent.php create mode 100644 src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php create mode 100644 src/FuzeWorks/Event/ViewGetEvent.php create mode 100644 src/FuzeWorks/Exception/ControllerException.php create mode 100644 src/FuzeWorks/Exception/HaltException.php create mode 100644 src/FuzeWorks/Exception/ModelException.php create mode 100644 src/FuzeWorks/Exception/NotFoundException.php create mode 100644 src/FuzeWorks/Exception/RouterException.php create mode 100644 src/FuzeWorks/Exception/ViewException.php create mode 100644 src/FuzeWorks/MVCRComponent.php create mode 100644 src/FuzeWorks/Model.php create mode 100644 src/FuzeWorks/Models.php create mode 100644 src/FuzeWorks/Router.php create mode 100644 src/FuzeWorks/View.php create mode 100644 src/FuzeWorks/Views.php create mode 100644 test/autoload.php create mode 100644 test/bootstrap.php create mode 100644 test/controllers/TestDifferentComponentPathPriority/Highest/controller.testdifferentcomponentpathpriority.php create mode 100644 test/controllers/TestDifferentComponentPathPriority/Lowest/controller.testdifferentcomponentpathpriority.php create mode 100644 test/controllers/TestGetSubdirectory/controller.testgetsubdirectory.php create mode 100644 test/controllers/controller.controllerinvalidinstance.php create mode 100644 test/controllers/controller.testcontrollergeteventintervene.php create mode 100644 test/controllers/controller.testdefaultcallable.php create mode 100644 test/controllers/controller.testdefaultcallablehalt.php create mode 100644 test/controllers/controller.testdefaultcallablemissingview.php create mode 100644 test/controllers/controller.testgetcontroller.php create mode 100644 test/mcr/ControllersTest.php create mode 100644 test/mcr/MVCRTestAbstract.php create mode 100644 test/mcr/ModelsTest.php create mode 100644 test/mcr/RouterTest.php create mode 100644 test/mcr/ViewsTest.php create mode 100644 test/models/TestDifferentComponentPathPriority/Highest/model.testdifferentcomponentpathpriority.php create mode 100644 test/models/TestDifferentComponentPathPriority/Lowest/model.testdifferentcomponentpathpriority.php create mode 100644 test/models/TestGetSubdirectory/model.testgetsubdirectory.php create mode 100644 test/models/model.modelinvalidinstance.php create mode 100644 test/models/model.testgetmodel.php create mode 100644 test/models/model.testmodelgeteventintervene.php create mode 100644 test/phpunit.xml create mode 100644 test/views/TestDifferentComponentPathPriority/Highest/view.standard.testdifferentcomponentpathpriority.php create mode 100644 test/views/TestDifferentComponentPathPriority/Lowest/view.standard.testdifferentcomponentpathpriority.php create mode 100644 test/views/TestGetSubdirectory/view.standard.testgetsubdirectory.php create mode 100644 test/views/view.othertype.testviewgeteventintervene.php create mode 100644 test/views/view.standard.testgetview.php create mode 100644 test/views/view.standard.viewinvalidinstance.php create mode 100644 test/views/view.test.testdefaultcallable.php create mode 100644 test/views/view.test.testdefaultcallablehalt.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c7b07b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +.gitattributes export-ignore +.gitignore export-ignore +.gitlab-ci.yml export-ignore +test/ export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb2af7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +composer.lock +composer.phar +.idea/ +build/ +test/temp/ +vendor/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82fedfb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2019 TechFuze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7d28cef --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "fuzeworks/mvcr", + "description": "FuzeWorks Framework MVC Component", + "license": ["MIT"], + "authors": [ + { + "name": "TechFuze", + "homepage": "https://techfuze.net" + }, + { + "name": "FuzeWorks Community", + "homepage": "https://techfuze.net/fuzeworks/contributors" + } + ], + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "fuzeworks/core": "dev-development", + "phpunit/phpunit": "^7", + "mikey179/vfsStream": "1.6.5" + }, + "autoload": { + "psr-4": { + "FuzeWorks\\": "src/FuzeWorks/" + } + } + +} \ No newline at end of file diff --git a/src/Config/config.routes.php b/src/Config/config.routes.php new file mode 100644 index 0000000..8503dce --- /dev/null +++ b/src/Config/config.routes.php @@ -0,0 +1,41 @@ + 'index', + 'default_viewType' => 'standard', + 'default_viewMethod' => 'index', + +); diff --git a/src/FuzeWorks/Controller.php b/src/FuzeWorks/Controller.php new file mode 100644 index 0000000..3d61683 --- /dev/null +++ b/src/FuzeWorks/Controller.php @@ -0,0 +1,49 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +abstract class Controller extends Factory +{ +} \ No newline at end of file diff --git a/src/FuzeWorks/Controllers.php b/src/FuzeWorks/Controllers.php new file mode 100644 index 0000000..e05c677 --- /dev/null +++ b/src/FuzeWorks/Controllers.php @@ -0,0 +1,172 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class Controllers +{ + use ComponentPathsTrait; + + /** + * Get a controller. + * + * Supply the name and the controller will be loaded from the supplied directory, + * or from one of the controllerPaths (which you can add). + * + * @param string $controllerName Name of the controller + * @param array $controllerPaths Alternative paths to use to load the controller + * @param string $namespace Alternative namespace for the controller. Defaults to \Application\Controller + * @param mixed %arguments,... Arguments to be provided to the constructor [...] + * @return Controller + * @throws ControllerException + * @throws NotFoundException + */ + public function get(string $controllerName, array $controllerPaths = [], string $namespace = '\Application\Controller\\'): Controller + { + if (empty($controllerName)) + throw new ControllerException("Could not load controller. No name provided", 1); + + // First get the directories where the controller can be located + $controllerPaths = (empty($controllerPaths) ? $this->componentPaths : [3 => $controllerPaths]); + + // Get arguments for constructor + if (func_num_args() > 3) + $arguments = array_slice(func_get_args(), 3); + else + $arguments = []; + + // Fire a controller load event + /** @var ControllerGetEvent $event */ + try { + $event = Events::fireEvent('controllerGetEvent', $controllerName, $controllerPaths, $namespace, $arguments); + } catch (EventException $e) { + throw new ControllerException("Could not load controller. controllerGetEvent threw exception: '".$e->getMessage()."'"); + } + + // If the event is cancelled, stop loading + if ($event->isCancelled()) + throw new ControllerException("Could not load controller. Controller cancelled by controllerGetEvent."); + + // And attempt to load the controller + return $this->loadController($event->controllerName, $event->controllerPaths, $event->namespace, $event->arguments); + } + + /** + * Load and return a controller. + * + * Supply the name and the controller will be loaded from one of the supplied directories + * + * @param string $controllerName Name of the controller + * @param array $controllerPaths + * @param string $namespace + * @param array $arguments + * @return Controller The Controller object + * @throws ControllerException + * @throws NotFoundException + */ + protected function loadController(string $controllerName, array $controllerPaths, string $namespace, array $arguments): Controller + { + // Now figure out the className and subdir + $class = trim($controllerName, '/'); + if (($last_slash = strrpos($class, '/')) !== FALSE) { + // Extract the path + $subdir = substr($class, 0, ++$last_slash); + + // Get the filename from the path + $class = substr($class, $last_slash); + } else { + $subdir = ''; + } + + // If the class already exists, return a new instance directly + $class = ucfirst($class); + $className = $namespace . $class . 'Controller'; + if (class_exists($className, false)) + { + $controller = new $className(...$arguments); + if (!$controller instanceof Controller) + throw new ControllerException("Could not load controller. Provided controllerName is not instance of \FuzeWorks\Controller"); + + return $controller; + } + + // Search for the controller file + for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) + { + if (!isset($controllerPaths[$i])) + continue; + + foreach ($controllerPaths[$i] as $directory) { + + // Determine the file + $file = $directory . DS . $subdir . "controller." . strtolower($class) . '.php'; + + // If it doesn't, try and load the file + if (file_exists($file)) { + include_once($file); + + // Test if provided class is instance of Controller + $controller = new $className(...$arguments); + if (!$controller instanceof Controller) + throw new ControllerException("Could not load controller. Provided controllerName is not instance of \FuzeWorks\Controller"); + + return $controller; + } + } + } + + // Maybe it's in a subdirectory with the same name as the class + if ($subdir === '') { + return $this->loadController($class . "/" . $class, $controllerPaths, $namespace, $arguments); + } + + throw new NotFoundException("Could not load controller. Controller was not found", 1); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Event/ControllerGetEvent.php b/src/FuzeWorks/Event/ControllerGetEvent.php new file mode 100644 index 0000000..c9a5782 --- /dev/null +++ b/src/FuzeWorks/Event/ControllerGetEvent.php @@ -0,0 +1,86 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class ControllerGetEvent extends Event +{ + /** + * The directories the controller can get loaded from. + * + * @var array + */ + public $controllerPaths = array(); + + /** + * The name of the controller to be loaded. + * + * @var string|null + */ + public $controllerName = null; + + /** + * The namespace of the controller to be loaded. Defaults to Application\Controller + * + * @var string + */ + public $namespace = '\Application\Controller\\'; + + /** + * Arguments provided to the constructor + * + * @var array + */ + public $arguments = []; + + public function init($controllerName, $controllerPaths, $namespace, $arguments) + { + $this->controllerName = $controllerName; + $this->controllerPaths = $controllerPaths; + $this->namespace = $namespace; + $this->arguments = $arguments; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Event/ModelGetEvent.php b/src/FuzeWorks/Event/ModelGetEvent.php new file mode 100644 index 0000000..0562149 --- /dev/null +++ b/src/FuzeWorks/Event/ModelGetEvent.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ + +class ModelGetEvent extends Event +{ + /** + * The directories the model can get loaded from. + * + * @var array + */ + public $modelPaths = array(); + + /** + * The name of the model to be loaded. + * + * @var string|null + */ + public $modelName = null; + + /** + * The namespace of the model to be loaded. Defaults to Application\Model + * + * @var string + */ + public $namespace = '\Application\Model\\'; + + /** + * Arguments provided to the constructor + * + * @var array + */ + public $arguments = []; + + public function init($modelName, $modelPaths, $namespace, $arguments) + { + $this->modelName = $modelName; + $this->modelPaths = $modelPaths; + $this->namespace = $namespace; + $this->arguments = $arguments; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php new file mode 100644 index 0000000..03cc4aa --- /dev/null +++ b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php @@ -0,0 +1,113 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class RouterLoadViewAndControllerEvent extends Event +{ + /** + * The name of the view + * + * @var string + */ + public $viewName; + + /** + * The type of view to be loaded + * + * @var string + */ + public $viewType; + + /** + * The function that will be loaded in the view + * + * @var string + */ + public $viewMethod; + + /** + * The parameters that will be provided to the function in the view + * + * @var string + */ + public $viewParameters; + + /** + * The route that resulted in this controller and view + * + * @var string + */ + public $route; + + /** + * A controller to be injected. + * + * @var Controller|null + */ + public $controller; + + public function init(string $viewName, string $viewType, string $viewMethod, string $viewParameters, string $route) + { + $this->viewName = $viewName; + $this->viewType = $viewType; + $this->viewMethod = $viewMethod; + $this->viewParameters = $viewParameters; + $this->route = $route; + } + + /** + * Override the controller to be provided to the view. + * + * @param Controller $controller + */ + public function overrideController(Controller $controller) + { + $this->controller = $controller; + } +} diff --git a/src/FuzeWorks/Event/ViewGetEvent.php b/src/FuzeWorks/Event/ViewGetEvent.php new file mode 100644 index 0000000..9b222fe --- /dev/null +++ b/src/FuzeWorks/Event/ViewGetEvent.php @@ -0,0 +1,101 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class ViewGetEvent extends Event +{ + /** + * The directories the view can get loaded from. + * + * @var array + */ + public $viewPaths = []; + + /** + * The name of the view to be loaded. + * + * @var string|null + */ + public $viewName = null; + + /** + * The type of view to be loaded. Eg: html, json, cli. + * + * @var string|null + */ + public $viewType = null; + + /** + * The namespace of the View to be loaded. Defaults to Application\View + * + * @var string + */ + public $namespace = '\Application\View\\'; + + /** + * Arguments provided to the constructor + * + * @var array + */ + public $arguments = []; + + /** + * @var Controller + */ + public $controller; + + public function init($viewName, $viewType, $viewPaths, $namespace, $controller, $arguments) + { + $this->viewName = $viewName; + $this->viewType = $viewType; + $this->viewPaths = $viewPaths; + $this->namespace = $namespace; + $this->controller = $controller; + $this->arguments = $arguments; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Exception/ControllerException.php b/src/FuzeWorks/Exception/ControllerException.php new file mode 100644 index 0000000..bc288c6 --- /dev/null +++ b/src/FuzeWorks/Exception/ControllerException.php @@ -0,0 +1,41 @@ + '\FuzeWorks\Controllers', + 'models' => '\FuzeWorks\Models', + 'views' => '\FuzeWorks\Views', + 'router' => '\FuzeWorks\Router' + ]; + } + + public function onAddComponent(Configurator $configurator) + { + // Add fallback config directory + $configurator->addDirectory( + dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Config', + 'config', + Priority::LOWEST + ); + } + + public function onCreateContainer(Factory $container) + { + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Model.php b/src/FuzeWorks/Model.php new file mode 100644 index 0000000..483cfd0 --- /dev/null +++ b/src/FuzeWorks/Model.php @@ -0,0 +1,50 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +abstract class Model extends Factory +{ +} diff --git a/src/FuzeWorks/Models.php b/src/FuzeWorks/Models.php new file mode 100644 index 0000000..02e5a5e --- /dev/null +++ b/src/FuzeWorks/Models.php @@ -0,0 +1,171 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class Models +{ + use ComponentPathsTrait; + + /** + * Get a model. + * + * Supply the name and the model will be loaded from the supplied directory, + * or from one of the modelPaths (which you can add). + * + * @param string $modelName Name of the model + * @param array $modelPaths Alternative paths to use to load the model + * @param string $namespace Alternative namespace for the model. Defaults to \Application\Model + * @param mixed %arguments,... Arguments to be provided to the constructor [...] + * @return Model + * @throws NotFoundException + * @throws ModelException + */ + public function get(string $modelName, array $modelPaths = [], string $namespace = '\Application\Model\\'): Model + { + if (empty($modelName)) + throw new ModelException("Could not load model. No name provided", 1); + + // First get the directories where the model can be located + $modelPaths = (empty($modelPaths) ? $this->componentPaths : [3 => $modelPaths]); + + // Get arguments for constructor + if (func_num_args() > 3) + $arguments = array_slice(func_get_args(), 3); + else + $arguments = []; + + // Fire a model load event + /** @var ModelGetEvent $event */ + try { + $event = Events::fireEvent('modelGetEvent', $modelName, $modelPaths, $namespace, $arguments); + } catch (EventException $e) { + throw new ModelException("Could not load model. modelGetEvent threw exception: '".$e->getMessage()."'"); + } + + // If the event is cancelled, stop loading + if ($event->isCancelled()) + throw new ModelException("Could not load model. Model cancelled by modelGetEvent."); + + // And attempt to load the model + return $this->loadModel($event->modelName, $event->modelPaths, $event->namespace, $event->arguments); + } + + /** + * Load and return a model. + * + * Supply the name and the model will be loaded from one of the supplied directories + * + * @param string $modelName Name of the model + * @param array $modelPaths + * @param string $namespace + * @param array $arguments + * @return Model The Model object + * @throws ModelException + * @throws NotFoundException + */ + protected function loadModel(string $modelName, array $modelPaths, string $namespace, array $arguments): Model + { + // Now figure out the className and subdir + $class = trim($modelName, '/'); + if (($last_slash = strrpos($class, '/')) !== FALSE) { + // Extract the path + $subdir = substr($class, 0, ++$last_slash); + + // Get the filename from the path + $class = substr($class, $last_slash); + } else { + $subdir = ''; + } + + // If the class already exists, return a new instance directly + $class = ucfirst($class); + $className = $namespace . $class . 'Model'; + if (class_exists($className, false)) { + $model = new $className(...$arguments); + if (!$model instanceof Model) + throw new ModelException("Could not load model. Provided modelName is not instance of \FuzeWorks\Model"); + + return $model; + } + + // Search for the model file + for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) + { + if (!isset($modelPaths[$i])) + continue; + + foreach ($modelPaths[$i] as $directory) { + + // Determine the file + $file = $directory . DS . $subdir . "model." . strtolower($class) . '.php'; + + // If it doesn't, try and load the file + if (file_exists($file)) { + include_once($file); + + // Test if provided class is instance of Model + $model = new $className(...$arguments); + if (!$model instanceof Model) + throw new ModelException("Could not load model. Provided modelName is not instance of \FuzeWorks\Model"); + + return $model; + } + } + } + + // Maybe it's in a subdirectory with the same name as the class + if ($subdir === '') { + return $this->loadModel($class . "/" . $class, $modelPaths, $namespace, $arguments); + } + + throw new NotFoundException("Could not load model. Model was not found", 1); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php new file mode 100644 index 0000000..1cf7e7a --- /dev/null +++ b/src/FuzeWorks/Router.php @@ -0,0 +1,400 @@ +config = $factory->config; + $this->controllers = $factory->controllers; + $this->views = $factory->views; + } + + /** + * Route Parser + * + * This method parses all the routes in the routes table config file + * and adds them to the Router. It converts some routes which use wildcards + * + * @return void + * @throws RouterException + */ + public function init() + { + // Get routing routes + try { + $routes = $this->config->getConfig('routes'); + // @codeCoverageIgnoreStart + } catch (ConfigException $e) { + throw new RouterException("Could not parse routing. Error in config 'routes'"); + // @codeCoverageIgnoreEnd + } + + // Cycle through all provided routes + foreach ($routes as $route => $routeConfig) + { + // Check if only a string is provided + // e.g: 0 => '.*$' + if (is_int($route)) + { + $route = $routeConfig; + $routeConfig = ['callable' => [$this, 'defaultCallable']]; + } + + // Finally add the route + $this->addRoute($route, $routeConfig); + } + } + + public function addRoute(string $route, $routeConfig = null, bool $prepend = true) + { + // Set defaultCallable if no value provided + if (is_null($routeConfig)) + $routeConfig = ['callable' => [$this, 'defaultCallable']]; + + // Convert wildcards to Regex + $route = str_replace([':any',':num'], ['[^/]+', '[0-9]+'], $route); + + if ($prepend) + $this->routes = [$route => $routeConfig] + $this->routes; + else + $this->routes[$route] = $routeConfig; + + Logger::log('Route added at '.($prepend ? 'top' : 'bottom').': "'.$route.'"'); + } + + /** + * Removes a route from the array based on the given route. + * + * @param $route string The route to remove + */ + public function removeRoute(string $route) + { + unset($this->routes[$route]); + + Logger::log('Route removed: '.$route); + } + + /** + * @param string $path + * @return mixed + * @throws NotFoundException + */ + public function route(string $path) + { + // Check all the provided custom paths + foreach ($this->routes as $route => $routeConfig) + { + // Match the path against the routes + if (!preg_match('#^'.$route.'$#', $path, $matches)) + continue; + + // Save the matches + Logger::log('Route matched: '.$route); + $this->matches = $matches; + $this->route = $route; + + // Call callable if routeConfig is callable, so routeConfig can be replaced + // e.g: '.*$' => callable + if (is_callable($routeConfig)) + $routeConfig = call_user_func_array($routeConfig, [$matches]); + + // If routeConfig is an array, multiple things might be at hand + if (is_array($routeConfig)) + { + // Replace defaultCallable if a custom callable is provided + // e.g: '.*$' => ['callable' => [$object, 'method']] + if (isset($routeConfig['callable']) && is_callable($routeConfig['callable'])) + $this->callable = $routeConfig['callable']; + + // If the route provides a configuration, use that + // e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index'] + else + $this->matches = array_merge($this->matches, $routeConfig); + } + + // If no custom callable is provided, use default + if (is_null($this->callable)) + $this->callable = [$this, 'defaultCallable']; + + // Attempt and load callable. If false, continue + $output = $this->loadCallable($this->callable, $this->matches, $route); + if (is_bool($output) && $output === FALSE) + { + Logger::log('Callable not satisfied, skipping to next callable'); + continue; + } + + return $output; + } + + throw new NotFoundException("Could not load view. Router could not find matching route."); + } + + /** + * @param callable $callable + * @param array $matches + * @param string $route + * @return mixed + */ + protected function loadCallable(callable $callable, array $matches, string $route) + { + // Log the input to the logger + Logger::newLevel('Loading callable with matches:'); + foreach ($matches as $key => $value) { + if (!is_int($key)) + Logger::log($key.': '.var_export($value, true).''); + } + Logger::stopLevel(); + + // Invoke callable + return call_user_func_array($callable, [$matches, $route]); + } + + /** + * @param array $matches + * @param string $route + * @return mixed + * @throws HaltException + * @throws RouterException + * @todo Use $route and send it to the view + */ + public function defaultCallable(array $matches, string $route) + { + Logger::log('defaultCallable called'); + + // Prepare variables + $viewName = isset($matches['viewName']) ? $matches['viewName'] : null; + $viewType = isset($matches['viewType']) ? $matches['viewType'] : $this->config->routing->default_viewType; + $viewMethod = isset($matches['viewMethod']) ? $matches['viewMethod'] : $this->config->routing->default_viewMethod; + $viewParameters = isset($matches['viewParameters']) ? $matches['viewParameters'] : ''; + + // If nothing is provided, cancel loading + if (is_null($viewName)) + return false; + + try { + /** @var RouterLoadViewAndControllerEvent $event */ + $event = Events::fireEvent('routerLoadViewAndControllerEvent', + $viewName, + $viewType, + $viewMethod, + $viewParameters, + $route + ); + } catch (EventException $e) { + throw new RouterException("Could not load view. routerLoadViewAndControllerEvent threw exception: '".$e->getMessage()."'"); + } + + // Cancel if requested to do so + if ($event->isCancelled()) + throw new HaltException("Will not load view. Cancelled by routerLoadViewAndControllerEvent."); + + // First receive the controller + try { + $this->controller = (!is_null($event->controller) ? $event->controller : $this->controllers->get($event->viewName)); + } catch (ControllerException $e) { + throw new RouterException("Could not load view. Controllers::get threw ControllerException: '".$e->getMessage()."'"); + } catch (NotFoundException $e) { + return false; + } + + // Then try and receive the view + try { + $this->view = $this->views->get($event->viewName, $this->controller, $event->viewType); + } catch (ViewException $e) { + throw new RouterException("Could not load view. Views::get threw ViewException: '".$e->getMessage()."'"); + } catch (NotFoundException $e) { + return false; + } + + // If the view does not want a function to be loaded, provide a halt parameter + if (isset($this->view->halt)) + throw new HaltException("Will not load view. Cancelled by 'halt' attribute in view."); + + // Check if requested function or magic method exists in view + if (method_exists($this->view, $event->viewMethod) || method_exists($this->view, '__call')) + { + // Run viewCallMethodEvent. + try { + $methodEvent = Events::fireEvent('viewCallMethodEvent'); + } catch (EventException $e) { + throw new RouterException("Could not load view. viewCallMethodEvent threw exception: '".$e->getMessage()."'"); + } + + // If cancelled, halt + if ($methodEvent->isCancelled()) + throw new HaltException("Will not load view. Cancelled by viewCallMethodEvent"); + + // Execute the function on the view + return $this->view->{$event->viewMethod}($event->viewParameters); + } + + // View could not be found + return false; + } + + /** + * Returns an array with all the routes. + * + * @return array + * @codeCoverageIgnore + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * Returns the current route + * + * @return string|null + * @codeCoverageIgnore + */ + public function getCurrentRoute() + { + return $this->route; + } + + /** + * Returns all the matches with the RegEx route. + * + * @return null|array + * @codeCoverageIgnore + */ + public function getCurrentMatches() + { + return $this->matches; + } + + /** + * Returns the current View + * + * @return View|null + * @codeCoverageIgnore + */ + public function getCurrentView() + { + return $this->view; + } + + /** + * Returns the current Controller + * + * @return Controller|null + * @codeCoverageIgnore + */ + public function getCurrentController() + { + return $this->controller; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/View.php b/src/FuzeWorks/View.php new file mode 100644 index 0000000..ccb5a81 --- /dev/null +++ b/src/FuzeWorks/View.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +abstract class View extends Factory +{ + + /** + * The controller associated with this view + * + * @var Controller + */ + protected $controller; + + /** + * Provide the View with its associated Controller + * + * @param Controller $controller + */ + public function setController(Controller $controller) + { + $this->controller = $controller; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Views.php b/src/FuzeWorks/Views.php new file mode 100644 index 0000000..1a77ce7 --- /dev/null +++ b/src/FuzeWorks/Views.php @@ -0,0 +1,179 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class Views +{ + use ComponentPathsTrait; + + /** + * Get a view. + * + * Supply the name and the view will be loaded from the supplied directory, + * or from one of the viewPaths (which you can add). + * + * @param string $viewName Name of the view + * @param Controller $controller + * @param string $viewType The type of view to be loaded. Defaults to 'Standard' + * @param array $viewPaths Alternative paths to use to load the view + * @param string $namespace Alternative namespace for the view. Defaults to \Application\View + * @param mixed %arguments,... Arguments to be provided to the constructor [...] + * @return View + * @throws NotFoundException + * @throws ViewException + */ + public function get(string $viewName, Controller $controller, string $viewType = 'Standard', array $viewPaths = [], string $namespace = '\Application\View\\'): View + { + if (empty($viewName)) + throw new ViewException("Could not load view. No name provided", 1); + + // First get the directories where the view can be located + $viewPaths = (empty($viewPaths) ? $this->componentPaths : [3 => $viewPaths]); + + // Get arguments for constructor + if (func_num_args() > 5) + $arguments = array_slice(func_get_args(), 5); + else + $arguments = []; + + // Fire a view load event + /** @var ViewGetEvent $event */ + try { + $event = Events::fireEvent('viewGetEvent', $viewName, $viewType, $viewPaths, $namespace, $controller, $arguments); + } catch (Exception\EventException $e) { + throw new ViewException("Could not load view. viewGetEvent threw exception: '".$e->getMessage()."''"); + } + + // If the event is cancelled, stop loading + if ($event->isCancelled()) + throw new ViewException("Could not load view. View cancelled by viewGetEvent."); + + // And attempt to load the view + return $this->loadView($event->viewName, $event->controller, $event->viewType, $event->viewPaths, $event->namespace, $event->arguments); + } + + /** + * Load and return a view. + * + * Supply the name and the view will be loaded from one of the supplied directories + * + * @param string $viewName Name of the view + * @param Controller $controller + * @param string $viewType Type of the view + * @param array $viewPaths + * @param string $namespace + * @param array $arguments + * @return View The View object + * @throws NotFoundException + * @throws ViewException + */ + protected function loadView(string $viewName, Controller $controller, string $viewType, array $viewPaths, string $namespace, array $arguments): View + { + // Now figure out the className and subdir + $class = trim($viewName, '/'); + if (($last_slash = strrpos($class, '/')) !== FALSE) { + // Extract the path + $subdir = substr($class, 0, ++$last_slash); + + // Get the filename from the path + $class = substr($class, $last_slash); + } else { + $subdir = ''; + } + + // If the class already exists, return a new instance directly + $class = ucfirst($class); + $className = $namespace . $class . $viewType . 'View'; + if (class_exists($className, false)) { + /** @var View $view */ + $view = new $className(...$arguments); + if (!$view instanceof View) + throw new ViewException("Could not load view. Provided viewName is not instance of \FuzeWorks\View"); + + // Load and return + $view->setController($controller); + return $view; + } + + // Search for the view file + for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) + { + if (!isset($viewPaths[$i])) + continue; + + foreach ($viewPaths[$i] as $directory) { + + // Determine the file + $file = $directory . DS . $subdir . "view." . strtolower($viewType) . "." . strtolower($class) . '.php'; + + // If it doesn't, try and load the file + if (file_exists($file)) { + include_once($file); + + /** @var View $view */ + $view = new $className(...$arguments); + if (!$view instanceof View) + throw new ViewException("Could not load view. Provided viewName is not instance of \FuzeWorks\View"); + + // Load and return + $view->setController($controller); + return $view; + } + } + } + + // Maybe it's in a subdirectory with the same name as the class + if ($subdir === '') { + return $this->loadView($class . "/" . $class, $controller, $viewType, $viewPaths, $namespace, $arguments); + } + + throw new NotFoundException("Could not load view. View was not found", 1); + } +} \ No newline at end of file diff --git a/test/autoload.php b/test/autoload.php new file mode 100644 index 0000000..03fa03c --- /dev/null +++ b/test/autoload.php @@ -0,0 +1,52 @@ +setTempDirectory(dirname(__FILE__) . '/temp'); +$configurator->setLogDirectory(dirname(__FILE__) . '/temp'); + +// Other values +$configurator->setTimeZone('Europe/Amsterdam'); +$configurator->enableDebugMode(false); +$configurator->setDebugAddress('NONE'); + +// Implement the MVCR Component +$configurator->addComponent(new \FuzeWorks\MVCRComponent()); + +// Create container +$container = $configurator->createContainer(); + +// And return the result +return $container; + diff --git a/test/controllers/TestDifferentComponentPathPriority/Highest/controller.testdifferentcomponentpathpriority.php b/test/controllers/TestDifferentComponentPathPriority/Highest/controller.testdifferentcomponentpathpriority.php new file mode 100644 index 0000000..55e9a81 --- /dev/null +++ b/test/controllers/TestDifferentComponentPathPriority/Highest/controller.testdifferentcomponentpathpriority.php @@ -0,0 +1,44 @@ +controllers = new Controllers(); + $this->controllers->addComponentPath('test'.DS.'controllers'); + } + + /** + * @covers ::get + * @covers ::loadController + */ + public function testGetControllerFromClass() + { + // Create mock controller + $mockController = $this->getMockBuilder(Controller::class)->getMock(); + $mockControllerClass = get_class($mockController); + class_alias($mockControllerClass, $mockControllerClass . 'Controller'); + + // Try and fetch this controller from the Controllers class + $this->assertInstanceOf($mockControllerClass, $this->controllers->get($mockControllerClass, [], '\\')); + } + + /** + * @depends testGetControllerFromClass + * @covers ::get + * @covers ::loadController + * @expectedException \FuzeWorks\Exception\ControllerException + */ + public function testGetControllerFromClassInvalidInstance() + { + // Create invalid mock + $mockFakeController = $this->getMockBuilder(stdClass::class)->getMock(); + $mockFakeControllerClass = get_class($mockFakeController); + class_alias($mockFakeControllerClass, $mockFakeControllerClass . 'Controller'); + + // Try and fetch + $this->controllers->get($mockFakeControllerClass, [], '\\'); + } + + /** + * @depends testGetControllerFromClass + * @covers ::get + * @covers ::loadController + */ + public function testGetControllerFromClassDefaultNamespace() + { + // Create mock controller + $mockController = $this->getMockBuilder(Controller::class)->getMock(); + $mockControllerClass = get_class($mockController); + class_alias($mockControllerClass, '\Application\Controller\DefaultNamespaceController'); + + // Try and fetch + $this->assertInstanceOf('\Application\Controller\DefaultNamespaceController', $this->controllers->get('DefaultNamespace')); + } + + /** + * @depends testGetControllerFromClass + * @covers ::get + * @covers ::loadController + * @todo Implement. Mock constructor arguments doesn't work yet + */ + public function testGetControllerWithArguments() + { + // Can't be tested right now + $this->assertTrue(true); + } + + /** + * @covers ::get + * @expectedException \FuzeWorks\Exception\ControllerException + */ + public function testGetControllerInvalidName() + { + $this->controllers->get('', [], '\\'); + } + + /** + * @depends testGetControllerFromClass + * @covers ::get + * @covers ::loadController + */ + public function testGetControllerFromFile() + { + $this->assertInstanceOf('\Application\Controller\TestGetControllerController', $this->controllers->get('TestGetController')); + } + + /** + * @depends testGetControllerFromFile + * @covers ::get + * @covers ::loadController + * @expectedException \FuzeWorks\Exception\ControllerException + */ + public function testGetControllerFromFileInvalidInstance() + { + $this->controllers->get('ControllerInvalidInstance'); + } + + /** + * @depends testGetControllerFromFile + * @covers ::get + * @covers ::loadController + */ + public function testDifferentComponentPathPriority() + { + // Add the directories for this test + $this->controllers->addComponentPath('test'.DS.'controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST); + $this->controllers->addComponentPath('test'.DS.'controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST); + + // Load the controller and assert it is the correct type + $controller = $this->controllers->get('TestDifferentComponentPathPriority'); + $this->assertInstanceOf('\Application\Controller\TestDifferentComponentPathPriorityController', $controller); + $this->assertEquals('highest', $controller->type); + + // Clean up the test + $this->controllers->setDirectories([]); + } + + /** + * @depends testGetControllerFromFile + * @covers ::get + * @covers ::loadController + */ + public function testGetSubdirectory() + { + $this->assertInstanceOf('\Application\Controller\TestGetSubdirectoryController', $this->controllers->get('TestGetSubdirectory')); + } + + /** + * @depends testGetControllerFromFile + * @covers ::get + * @covers ::loadController + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testControllerNotFound() + { + $this->controllers->get('NotFound'); + } + + /** + * @depends testGetControllerFromClass + * @covers ::get + * @covers \FuzeWorks\Event\ControllerGetEvent::init + * @expectedException \FuzeWorks\Exception\ControllerException + */ + public function testControllerGetEvent() + { + // Register listener + Events::addListener(function($event){ + /** @var ControllerGetEvent $event */ + $this->assertInstanceOf('\FuzeWorks\Event\ControllerGetEvent', $event); + $this->assertEquals('SomeControllerName', $event->controllerName); + $this->assertEquals([3 => ['some_path']], $event->controllerPaths); + $this->assertEquals('SomeNamespace', $event->namespace); + $this->assertEquals(['Some Argument'], $event->arguments); + $event->setCancelled(true); + }, 'controllerGetEvent', Priority::NORMAL); + + $this->controllers->get('SomeControllerName', ['some_path'], 'SomeNamespace', 'Some Argument'); + } + + /** + * @depends testControllerGetEvent + * @covers ::get + * @expectedException \FuzeWorks\Exception\ControllerException + */ + public function testCancelGetController() + { + // Register listener + Events::addListener(function($event){ + $event->setCancelled(true); + }, 'controllerGetEvent', Priority::NORMAL); + + $this->controllers->get('SomeController', [], '\\'); + } + + /** + * @depends testControllerGetEvent + * @covers ::get + * @covers ::loadController + */ + public function testControllerGetEventIntervene() + { + // Register listener + Events::addListener(function($event){ + /** @var ControllerGetEvent $event */ + $event->controllerName = 'TestControllerGetEventIntervene'; + $event->namespace = '\Some\Other\\'; + }, 'controllerGetEvent', Priority::NORMAL); + + $this->assertInstanceOf('\Some\Other\TestControllerGetEventInterveneController', $this->controllers->get('Something_Useless')); + } + +} diff --git a/test/mcr/MVCRTestAbstract.php b/test/mcr/MVCRTestAbstract.php new file mode 100644 index 0000000..86850c3 --- /dev/null +++ b/test/mcr/MVCRTestAbstract.php @@ -0,0 +1,60 @@ +config->discardConfigFiles(); + } +} diff --git a/test/mcr/ModelsTest.php b/test/mcr/ModelsTest.php new file mode 100644 index 0000000..50a8683 --- /dev/null +++ b/test/mcr/ModelsTest.php @@ -0,0 +1,246 @@ +models = new Models(); + $this->models->addComponentPath('test'.DS.'models'); + } + + /** + * @covers ::get + * @covers ::loadModel + */ + public function testGetModelFromClass() + { + // Create mock model + $mockModel = $this->getMockBuilder(Model::class)->getMock(); + $mockModelClass = get_class($mockModel); + class_alias($mockModelClass, $mockModelClass . 'Model'); + + // Try and fetch this model from the Models class + $this->assertInstanceOf($mockModelClass, $this->models->get($mockModelClass, [], '\\')); + } + + /** + * @depends testGetModelFromClass + * @covers ::get + * @covers ::loadModel + * @expectedException \FuzeWorks\Exception\ModelException + */ + public function testGetModelFromClassInvalidInstance() + { + // Create invalid mock + $mockFakeModel = $this->getMockBuilder(stdClass::class)->getMock(); + $mockFakeModelClass = get_class($mockFakeModel); + class_alias($mockFakeModelClass, $mockFakeModelClass . 'Model'); + + // Try and fetch + $this->models->get($mockFakeModelClass, [], '\\'); + } + + /** + * @depends testGetModelFromClass + * @covers ::get + * @covers ::loadModel + */ + public function testGetModelFromClassDefaultNamespace() + { + // Create mock model + $mockModel = $this->getMockBuilder(Model::class)->getMock(); + $mockModelClass = get_class($mockModel); + class_alias($mockModelClass, '\Application\Model\DefaultNamespaceModel'); + + // Try and fetch + $this->assertInstanceOf('\Application\Model\DefaultNamespaceModel', $this->models->get('DefaultNamespace')); + } + + /** + * @depends testGetModelFromClass + * @covers ::get + * @covers ::loadModel + * @todo Implement. Mock constructor arguments doesn't work yet + */ + public function testGetModelWithArguments() + { + // Can't be tested right now + $this->assertTrue(true); + } + + /** + * @covers ::get + * @expectedException \FuzeWorks\Exception\ModelException + */ + public function testGetModelInvalidName() + { + $this->models->get('', [], '\\'); + } + + /** + * @depends testGetModelFromClass + * @covers ::get + * @covers ::loadModel + */ + public function testGetModelFromFile() + { + $this->assertInstanceOf('\Application\Model\TestGetModelModel', $this->models->get('TestGetModel')); + } + + /** + * @depends testGetModelFromFile + * @covers ::get + * @covers ::loadModel + * @expectedException \FuzeWorks\Exception\ModelException + */ + public function testGetModelFromFileInvalidInstance() + { + $this->models->get('ModelInvalidInstance'); + } + + /** + * @depends testGetModelFromFile + * @covers ::get + * @covers ::loadModel + */ + public function testDifferentComponentPathPriority() + { + // Add the directories for this test + $this->models->addComponentPath('test'.DS.'models'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST); + $this->models->addComponentPath('test'.DS.'models'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST); + + // Load the model and assert it is the correct type + $model = $this->models->get('TestDifferentComponentPathPriority'); + $this->assertInstanceOf('\Application\Model\TestDifferentComponentPathPriorityModel', $model); + $this->assertEquals('highest', $model->type); + + // Clean up the test + $this->models->setDirectories([]); + } + + /** + * @depends testGetModelFromFile + * @covers ::get + * @covers ::loadModel + */ + public function testGetSubdirectory() + { + $this->assertInstanceOf('\Application\Model\TestGetSubdirectoryModel', $this->models->get('TestGetSubdirectory')); + } + + /** + * @depends testGetModelFromFile + * @covers ::get + * @covers ::loadModel + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testModelNotFound() + { + $this->models->get('NotFound'); + } + + /** + * @depends testGetModelFromClass + * @covers ::get + * @covers \FuzeWorks\Event\ModelGetEvent::init + * @expectedException \FuzeWorks\Exception\ModelException + */ + public function testModelGetEvent() + { + // Register listener + Events::addListener(function($event){ + /** @var ModelGetEvent $event */ + $this->assertInstanceOf('\FuzeWorks\Event\ModelGetEvent', $event); + $this->assertEquals('SomeModelName', $event->modelName); + $this->assertEquals([3 => ['some_path']], $event->modelPaths); + $this->assertEquals('SomeNamespace', $event->namespace); + $this->assertEquals(['Some Argument'], $event->arguments); + $event->setCancelled(true); + }, 'modelGetEvent', Priority::NORMAL); + + $this->models->get('SomeModelName', ['some_path'], 'SomeNamespace', 'Some Argument'); + } + + /** + * @depends testModelGetEvent + * @covers ::get + * @expectedException \FuzeWorks\Exception\ModelException + */ + public function testCancelGetModel() + { + // Register listener + Events::addListener(function($event){ + $event->setCancelled(true); + }, 'modelGetEvent', Priority::NORMAL); + + $this->models->get('SomeModel', [], '\\'); + } + + /** + * @depends testModelGetEvent + * @covers ::get + * @covers ::loadModel + */ + public function testModelGetEventIntervene() + { + // Register listener + Events::addListener(function($event){ + /** @var ModelGetEvent $event */ + $event->modelName = 'TestModelGetEventIntervene'; + $event->namespace = '\Some\Other\\'; + }, 'modelGetEvent', Priority::NORMAL); + + $this->assertInstanceOf('\Some\Other\TestModelGetEventInterveneModel', $this->models->get('Something_Useless')); + } + +} diff --git a/test/mcr/RouterTest.php b/test/mcr/RouterTest.php new file mode 100644 index 0000000..464a23d --- /dev/null +++ b/test/mcr/RouterTest.php @@ -0,0 +1,319 @@ +router = new Router(); + $this->config = Factory::getInstance()->config; + + // Append required routes + Factory::getInstance()->controllers->addComponentPath('test' . DS . 'controllers'); + Factory::getInstance()->views->addComponentPath('test' . DS . 'views'); + } + + /** + * @coversNothing + */ + public function testGetRouterClass() + { + $this->assertInstanceOf('FuzeWorks\Router', $this->router); + } + + /* Route Parsing ------------------------------------------------------ */ + + /** + * @depends testGetRouterClass + * @covers ::addRoute + * @covers ::getRoutes + */ + public function testAddRoutes() + { + $routeConfig = function () { + }; + $this->router->addRoute('testRoute', $routeConfig); + $this->assertArrayHasKey('testRoute', $this->router->getRoutes()); + $this->assertEquals($routeConfig, $this->router->getRoutes()['testRoute']); + } + + /** + * @depends testAddRoutes + * @covers ::addRoute + * @covers ::getRoutes + */ + public function testAddBlankRoute() + { + $this->router->addRoute('testBlankRoute'); + $this->assertArrayHasKey('testBlankRoute', $this->router->getRoutes()); + $this->assertEquals(['callable' => [$this->router, 'defaultCallable']], $this->router->getRoutes()['testBlankRoute']); + } + + /** + * @depends testAddRoutes + * @covers ::addRoute + * @covers ::getRoutes + */ + public function testAppendRoutes() + { + $testRouteFunction = [function () { + }]; + $testAppendRouteFunction = [function () { + }]; + $this->router->addRoute('testRoute', $testRouteFunction); + $this->router->addRoute('testAppendRoute', $testAppendRouteFunction, false); + + // Test if the order is correct + $this->assertSame( + ['testRoute' => $testRouteFunction, 'testAppendRoute' => $testAppendRouteFunction], + $this->router->getRoutes() + ); + + // Test if the order is not incorrect + $this->assertNotSame( + ['testAppendRoute' => $testAppendRouteFunction, 'testRoute' => $testRouteFunction], + $this->router->getRoutes() + ); + } + + /** + * @depends testAddRoutes + * @covers ::addRoute + * @covers ::getRoutes + * @covers ::removeRoute + */ + public function testRemoveRoutes() + { + // First add routes + $this->router->addRoute('testRemoveRoute', function () { + }); + $this->assertArrayHasKey('testRemoveRoute', $this->router->getRoutes()); + + // Then remove + $this->router->removeRoute('testRemoveRoute'); + $this->assertArrayNotHasKey('testRemoveRoute', $this->router->getRoutes()); + } + + /** + * @depends testAddRoutes + * @covers ::init + * @covers ::addRoute + */ + public function testParseRouting() + { + // Prepare the routes so they can be parsed + $this->config->routes->set('testParseRouting', function () { + }); + $this->router->init(); + + // Now verify whether the passing has been processed correctly + $this->assertArrayHasKey('testParseRouting', $this->router->getRoutes()); + } + + /** + * @depends testParseRouting + * @covers ::init + */ + public function testWildcardParsing() + { + // Prepare the routes so they can be parsed + $this->config->routes->set('testWildcardParsing/:any/:num', function () { + }); + $this->router->init(); + + // Now verify whether the route has been skipped + $this->assertArrayHasKey('testWildcardParsing/[^/]+/[0-9]+', $this->router->getRoutes()); + } + + /** + * @depends testParseRouting + * @covers ::init + */ + public function testBlankRouteParsing() + { + // Prepare the routes so they can be parsed + $this->config->routes->set(0, 'testBlankRouteParsing'); + $this->router->init(); + + // Now verify whether the route has been parsed + $this->assertArrayHasKey('testBlankRouteParsing', $this->router->getRoutes()); + } + + /* defaultCallable() -------------------------------------------------- */ + + /** + * @depends testGetRouterClass + * @covers ::defaultCallable + */ + public function testDefaultCallable() + { + $matches = [ + 'viewName' => 'TestDefaultCallable', + 'viewType' => 'test', + 'viewMethod' => 'someMethod' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->assertEquals('Verify Output', $this->router->defaultCallable($matches, '.*$')); + $this->assertInstanceOf('\Application\Controller\TestDefaultCallableController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestDefaultCallableTestView', $this->router->getCurrentView()); + } + + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + */ + public function testDefaultCallableMissingMethod() + { + $matches = [ + 'viewName' => 'TestDefaultCallable', + 'viewType' => 'test', + 'viewMethod' => 'missing' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->assertFalse($this->router->defaultCallable($matches, '.*$')); + $this->assertInstanceOf('\Application\Controller\TestDefaultCallableController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestDefaultCallableTestView', $this->router->getCurrentView()); + } + + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + */ + public function testDefaultCallableMissingView() + { + $matches = [ + 'viewName' => 'TestDefaultCallableMissingView', + 'viewType' => 'test', + 'viewMethod' => 'missing' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->assertFalse($this->router->defaultCallable($matches, '.*$')); + $this->assertInstanceOf('\Application\Controller\TestDefaultCallableMissingViewController', $this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + } + + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + */ + public function testDefaultCallableMissingController() + { + $matches = [ + 'viewName' => 'TestDefaultCallableMissingController', + 'viewType' => 'test', + 'viewMethod' => 'missing' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->assertFalse($this->router->defaultCallable($matches, '.*$')); + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + } + + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + * @expectedException \FuzeWorks\Exception\HaltException + */ + public function testDefaultCallableHalt() + { + $matches = [ + 'viewName' => 'TestDefaultCallableHalt', + 'viewType' => 'test', + 'viewMethod' => 'someMethod' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->router->defaultCallable($matches, '.*$'); + $this->assertInstanceOf('\Application\Controller\TestDefaultCallableHaltController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestDefaultCallableHaltTestView', $this->router->getCurrentView()); + } + + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + */ + public function testDefaultCallableEmptyName() + { + $matches = [ + 'viewType' => 'test', + 'viewMethod' => 'someMethod' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + $this->assertFalse($this->router->defaultCallable($matches, '.*$')); + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + } + + /* route() ------------------------------------------------------------ */ + + + +} diff --git a/test/mcr/ViewsTest.php b/test/mcr/ViewsTest.php new file mode 100644 index 0000000..2b7e3f6 --- /dev/null +++ b/test/mcr/ViewsTest.php @@ -0,0 +1,255 @@ +views = new Views(); + $this->views->addComponentPath('test'.DS.'views'); + $this->mockController = $this->getMockBuilder(Controller::class)->getMock(); + } + + /** + * @covers ::get + * @covers ::loadView + */ + public function testGetViewFromClass() + { + // Create mock view + $mockView = $this->getMockBuilder(View::class)->getMock(); + $mockViewClass = get_class($mockView); + class_alias($mockViewClass, $mockViewClass . 'StandardView'); + + // Try and fetch this view from the Views class + $this->assertInstanceOf($mockViewClass, $this->views->get($mockViewClass, $this->mockController, 'Standard', [], '\\')); + } + + /** + * @depends testGetViewFromClass + * @covers ::get + * @covers ::loadView + * @expectedException \FuzeWorks\Exception\ViewException + */ + public function testGetViewFromClassInvalidInstance() + { + // Create invalid mock + $mockFakeView = $this->getMockBuilder(stdClass::class)->getMock(); + $mockFakeViewClass = get_class($mockFakeView); + class_alias($mockFakeViewClass, $mockFakeViewClass . 'StandardView'); + + // Try and fetch + $this->views->get($mockFakeViewClass, $this->mockController, 'Standard', [], '\\'); + } + + /** + * @depends testGetViewFromClass + * @covers ::get + * @covers ::loadView + */ + public function testGetViewFromClassDefaultNamespace() + { + // Create mock view + $mockView = $this->getMockBuilder(View::class)->getMock(); + $mockViewClass = get_class($mockView); + class_alias($mockViewClass, '\Application\View\DefaultNamespaceStandardView'); + + // Try and fetch + $this->assertInstanceOf('\Application\View\DefaultNamespaceStandardView', $this->views->get('DefaultNamespace', $this->mockController)); + } + + /** + * @depends testGetViewFromClass + * @covers ::get + * @covers ::loadView + * @todo Implement. Mock constructor arguments doesn't work yet + */ + public function testGetViewWithArguments() + { + // Can't be tested right now + $this->assertTrue(true); + } + + /** + * @covers ::get + * @expectedException \FuzeWorks\Exception\ViewException + */ + public function testGetViewInvalidName() + { + $this->views->get('', $this->mockController, 'Standard', [], '\\'); + } + + /** + * @depends testGetViewFromClass + * @covers ::get + * @covers ::loadView + */ + public function testGetViewFromFile() + { + $this->assertInstanceOf('\Application\View\TestGetViewStandardView', $this->views->get('TestGetView', $this->mockController)); + } + + /** + * @depends testGetViewFromFile + * @covers ::get + * @covers ::loadView + * @expectedException \FuzeWorks\Exception\ViewException + */ + public function testGetViewFromFileInvalidInstance() + { + $this->views->get('ViewInvalidInstance', $this->mockController); + } + + /** + * @depends testGetViewFromFile + * @covers ::get + * @covers ::loadView + */ + public function testDifferentComponentPathPriority() + { + // Add the directories for this test + $this->views->addComponentPath('test'.DS.'views'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST); + $this->views->addComponentPath('test'.DS.'views'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST); + + // Load the view and assert it is the correct type + $view = $this->views->get('TestDifferentComponentPathPriority', $this->mockController); + $this->assertInstanceOf('\Application\View\TestDifferentComponentPathPriorityStandardView', $view); + $this->assertEquals('highest', $view->type); + + // Clean up the test + $this->views->setDirectories([]); + } + + /** + * @depends testGetViewFromFile + * @covers ::get + * @covers ::loadView + */ + public function testGetSubdirectory() + { + $this->assertInstanceOf('\Application\View\TestGetSubdirectoryStandardView', $this->views->get('TestGetSubdirectory', $this->mockController)); + } + + /** + * @depends testGetViewFromFile + * @covers ::get + * @covers ::loadView + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testViewNotFound() + { + $this->views->get('NotFound', $this->mockController); + } + + /** + * @depends testGetViewFromClass + * @covers ::get + * @covers \FuzeWorks\Event\ViewGetEvent::init + * @expectedException \FuzeWorks\Exception\ViewException + */ + public function testViewGetEvent() + { + // Register listener + Events::addListener(function($event){ + /** @var ViewGetEvent $event */ + $this->assertInstanceOf('\FuzeWorks\Event\ViewGetEvent', $event); + $this->assertEquals('SomeViewName', $event->viewName); + $this->assertInstanceOf('\FuzeWorks\Controller', $event->controller); + $this->assertEquals('Other', $event->viewType); + $this->assertEquals([3 => ['some_path']], $event->viewPaths); + $this->assertEquals('SomeNamespace', $event->namespace); + $this->assertEquals(['Some Argument'], $event->arguments); + $event->setCancelled(true); + }, 'viewGetEvent', Priority::NORMAL); + + $this->views->get('SomeViewName', $this->mockController, 'Other', ['some_path'], 'SomeNamespace', 'Some Argument'); + } + + /** + * @depends testViewGetEvent + * @covers ::get + * @expectedException \FuzeWorks\Exception\ViewException + */ + public function testCancelGetView() + { + // Register listener + Events::addListener(function($event){ + $event->setCancelled(true); + }, 'viewGetEvent', Priority::NORMAL); + + $this->views->get('SomeView', $this->mockController, 'Standard', [], '\\'); + } + + /** + * @depends testViewGetEvent + * @covers ::get + * @covers ::loadView + */ + public function testViewGetEventIntervene() + { + // Register listener + Events::addListener(function($event){ + /** @var ViewGetEvent $event */ + $event->viewName = 'TestViewGetEventIntervene'; + $event->viewType = 'OtherType'; + $event->namespace = '\Some\Other\\'; + }, 'viewGetEvent', Priority::NORMAL); + + $this->assertInstanceOf('\Some\Other\TestViewGetEventInterveneOtherTypeView', $this->views->get('Something_Useless', $this->mockController)); + } +} diff --git a/test/models/TestDifferentComponentPathPriority/Highest/model.testdifferentcomponentpathpriority.php b/test/models/TestDifferentComponentPathPriority/Highest/model.testdifferentcomponentpathpriority.php new file mode 100644 index 0000000..c45fee3 --- /dev/null +++ b/test/models/TestDifferentComponentPathPriority/Highest/model.testdifferentcomponentpathpriority.php @@ -0,0 +1,44 @@ + + + + + ./ + + + + + + + + + + + + + ../ + + ../vendor/ + ../tests/ + + + + \ No newline at end of file diff --git a/test/views/TestDifferentComponentPathPriority/Highest/view.standard.testdifferentcomponentpathpriority.php b/test/views/TestDifferentComponentPathPriority/Highest/view.standard.testdifferentcomponentpathpriority.php new file mode 100644 index 0000000..796c52f --- /dev/null +++ b/test/views/TestDifferentComponentPathPriority/Highest/view.standard.testdifferentcomponentpathpriority.php @@ -0,0 +1,44 @@ + Date: Fri, 1 Feb 2019 11:57:41 +0100 Subject: [PATCH 02/16] Implemented minor documentation and continuous integration --- .gitlab-ci.yml | 80 ++++++++++++++++++++++++++++++++++++ .travis.yml | 12 ++++++ src/Config/config.routes.php | 19 ++++++++- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 .gitlab-ci.yml create mode 100644 .travis.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..37621d3 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,80 @@ +before_script: +# Install dependencies +- set -xe +- apt-get update -yqq +- apt-get install git zip unzip -yqq + +stages: + - build + - test + - deploy + +build:composer: + image: php:7.2 + stage: build + script: + - curl -sS https://getcomposer.org/installer | php + - php composer.phar install + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor/ + +test:7.1: + stage: test + image: php:7.1 + script: + - vendor/bin/phpunit -c test/phpunit.xml + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor + +test:7.2: + stage: test + image: php:7.2 + script: + - vendor/bin/phpunit -c test/phpunit.xml + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor/ + +test:7.3: + stage: test + image: php:7.3 + script: + - vendor/bin/phpunit -c test/phpunit.xml + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor/ + +test:coverage: + stage: test + image: php:7.2 + script: + - pecl install xdebug + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --coverage-text + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor/ + +release: + stage: deploy + image: php:7.2 + only: + - master + script: + - vendor/bin/phpunit -c test/phpunit.xml --coverage-text + artifacts: + name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}" + paths: + - build/ + expire_in: 3 weeks + cache: + key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME" + paths: + - vendor/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4930ba4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + +script: + - php vendor/bin/phpunit -v -c test/phpunit.xml --coverage-text + +before_script: + - composer install diff --git a/src/Config/config.routes.php b/src/Config/config.routes.php index 8503dce..3f4a2e3 100644 --- a/src/Config/config.routes.php +++ b/src/Config/config.routes.php @@ -35,7 +35,24 @@ */ /** - * @todo Add documentation + * A route consists of two parts: a 'routeString' and 'routeConfig'. The routeString will be matched against the provided path. + * + * Possible values: + * Default callable: Adds a route that changes the URL structure. Sends all matches to the defaultCallable router + * 'routingString' + * + * Custom callable: Adds a route that sends all matches to the provided callable. Allows user to replace defaultCallable + * 'routingString' => array('callable' => array(CALLABLE)) + * + * Dynamic rewrite: Adds a route that rewrites an URL to a specific controller and method configuration, using a callable. The callable can dynamically determine which page to load. + * 'routingString' => CALLABLE + * + * Static rewrite: Adds a route that rewrites and URL to a specific controller and method using a fixed route. This allows for pre-determined rewrites of pages. + * 'routingString' => ['viewType' => 'someType', 'viewName' => 'someName', 'viewMethod' => 'someMethod', 'viewParameters' => 'someParameters'] + * + * Example routingString: '/^(?P.*?)(|\/(?P.*?)(|\/(?P.*?)))(|\.(?P.*?))$/' + * A routeString has to contain viewName, viewMethod, viewParameters and viewType in order to be processed by defaultCallable. */ + return array( ); From 3edbe99f35cdb8b25b46f0acc22302d74318124b Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 1 Feb 2019 12:02:15 +0100 Subject: [PATCH 03/16] Added placeholder file for temp directory, otherwise unit testing doesn't work. --- test/temp/placeholder | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/temp/placeholder diff --git a/test/temp/placeholder b/test/temp/placeholder new file mode 100644 index 0000000..e69de29 From 3613a04f493cfb29ed97ce0a79e4e905f09265fe Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 11 Feb 2019 19:23:45 +0100 Subject: [PATCH 04/16] Implemented unit tests for Router. Fixes #4 --- .gitlab-ci.yml | 4 +- .../RouterLoadViewAndControllerEvent.php | 2 +- src/FuzeWorks/Router.php | 36 ++- test/mcr/RouterTest.php | 212 +++++++++++++++++- test/phpunit.xml | 2 +- 5 files changed, 231 insertions(+), 25 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 37621d3..14bb468 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,7 +68,9 @@ release: only: - master script: - - vendor/bin/phpunit -c test/phpunit.xml --coverage-text + - pecl install xdebug + - docker-php-ext-enable xdebug + - vendor/bin/phpunit -c test/phpunit.xml --coverage-text artifacts: name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}" paths: diff --git a/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php index 03cc4aa..d072c33 100644 --- a/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php +++ b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php @@ -43,7 +43,7 @@ use FuzeWorks\Event; * * Use this to cancel the loading of a combination, or change the details of what is loaded. * - * Currently only used by OldRouter::defaultCallable(); + * Currently only used by Router::defaultCallable(); * * @author Abel Hoogeveen * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index 1cf7e7a..e697ea3 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -36,7 +36,6 @@ namespace FuzeWorks; - use FuzeWorks\Event\RouterLoadViewAndControllerEvent; use FuzeWorks\Exception\ConfigException; use FuzeWorks\Exception\ControllerException; @@ -204,6 +203,7 @@ class Router $this->route = $route; // Call callable if routeConfig is callable, so routeConfig can be replaced + // This is an example of 'Dynamic Rewrite' // e.g: '.*$' => callable if (is_callable($routeConfig)) $routeConfig = call_user_func_array($routeConfig, [$matches]); @@ -212,17 +212,20 @@ class Router if (is_array($routeConfig)) { // Replace defaultCallable if a custom callable is provided + // This is an example of 'Custom Callable' // e.g: '.*$' => ['callable' => [$object, 'method']] if (isset($routeConfig['callable']) && is_callable($routeConfig['callable'])) $this->callable = $routeConfig['callable']; // If the route provides a configuration, use that + // This is an example of 'Static Rewrite' // e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index'] else $this->matches = array_merge($this->matches, $routeConfig); } // If no custom callable is provided, use default + // This is an example of 'Default Callable' if (is_null($this->callable)) $this->callable = [$this, 'defaultCallable']; @@ -237,7 +240,7 @@ class Router return $output; } - throw new NotFoundException("Could not load view. Router could not find matching route."); + throw new NotFoundException("Could not load view. Router could not find matching route with satisfied callable."); } /** @@ -273,14 +276,10 @@ class Router Logger::log('defaultCallable called'); // Prepare variables - $viewName = isset($matches['viewName']) ? $matches['viewName'] : null; - $viewType = isset($matches['viewType']) ? $matches['viewType'] : $this->config->routing->default_viewType; - $viewMethod = isset($matches['viewMethod']) ? $matches['viewMethod'] : $this->config->routing->default_viewMethod; - $viewParameters = isset($matches['viewParameters']) ? $matches['viewParameters'] : ''; - - // If nothing is provided, cancel loading - if (is_null($viewName)) - return false; + $viewName = !empty($matches['viewName']) ? $matches['viewName'] : $this->config->routing->default_view; + $viewType = !empty($matches['viewType']) ? $matches['viewType'] : $this->config->routing->default_viewType; + $viewMethod = !empty($matches['viewMethod']) ? $matches['viewMethod'] : $this->config->routing->default_viewMethod; + $viewParameters = !empty($matches['viewParameters']) ? $matches['viewParameters'] : ''; try { /** @var RouterLoadViewAndControllerEvent $event */ @@ -305,6 +304,7 @@ class Router } catch (ControllerException $e) { throw new RouterException("Could not load view. Controllers::get threw ControllerException: '".$e->getMessage()."'"); } catch (NotFoundException $e) { + Logger::logError("Could not load view. Controller does not exist."); return false; } @@ -314,6 +314,7 @@ class Router } catch (ViewException $e) { throw new RouterException("Could not load view. Views::get threw ViewException: '".$e->getMessage()."'"); } catch (NotFoundException $e) { + Logger::logError("Could not load view. View does not exist."); return false; } @@ -324,20 +325,13 @@ class Router // Check if requested function or magic method exists in view if (method_exists($this->view, $event->viewMethod) || method_exists($this->view, '__call')) { - // Run viewCallMethodEvent. - try { - $methodEvent = Events::fireEvent('viewCallMethodEvent'); - } catch (EventException $e) { - throw new RouterException("Could not load view. viewCallMethodEvent threw exception: '".$e->getMessage()."'"); - } - - // If cancelled, halt - if ($methodEvent->isCancelled()) - throw new HaltException("Will not load view. Cancelled by viewCallMethodEvent"); - // Execute the function on the view return $this->view->{$event->viewMethod}($event->viewParameters); } + else + { + Logger::logError("Could not load view. View does not have method '".$event->viewMethod."'"); + } // View could not be found return false; diff --git a/test/mcr/RouterTest.php b/test/mcr/RouterTest.php index 464a23d..cb6641f 100644 --- a/test/mcr/RouterTest.php +++ b/test/mcr/RouterTest.php @@ -35,7 +35,9 @@ */ use FuzeWorks\Config; +use FuzeWorks\Events; use FuzeWorks\Factory; +use FuzeWorks\Priority; use FuzeWorks\Router; /** @@ -279,7 +281,7 @@ class RouterTest extends MVCRTestAbstract * @covers ::defaultCallable * @expectedException \FuzeWorks\Exception\HaltException */ - public function testDefaultCallableHalt() + public function testDefaultCallableHaltByView() { $matches = [ 'viewName' => 'TestDefaultCallableHalt', @@ -312,8 +314,216 @@ class RouterTest extends MVCRTestAbstract $this->assertNull($this->router->getCurrentView()); } + /** + * @depends testDefaultCallable + * @covers ::defaultCallable + * @covers \FuzeWorks\Event\RouterLoadViewAndControllerEvent::init + * @expectedException \FuzeWorks\Exception\HaltException + */ + public function testDefaultCallableHaltByEvent() + { + $matches = [ + 'viewName' => 'TestDefaultCallable', + 'viewType' => 'test', + 'viewMethod' => 'missing' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + + // Create listener + Events::addListener(function($event){ + $this->assertInstanceOf('\FuzeWorks\Event\RouterLoadViewAndControllerEvent', $event); + $this->assertEquals('TestDefaultCallable', $event->viewName); + $this->assertEquals('test', $event->viewType); + $this->assertEquals('missing', $event->viewMethod); + $event->setCancelled(true); + }, 'routerLoadViewAndControllerEvent'); + + $this->router->defaultCallable($matches, '.*$'); + } + + /** + * @depends testDefaultCallableHaltByEvent + * @covers ::defaultCallable + * @covers \FuzeWorks\Event\RouterLoadViewAndControllerEvent::overrideController + */ + public function testDefaultCallableReplaceController() + { + $matches = [ + 'viewName' => 'TestDefaultCallable', + 'viewType' => 'test', + 'viewMethod' => 'missing' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + + $mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock(); + + // Create listener + Events::addListener(function($event, $mockController){ + $event->overrideController($mockController); + }, 'routerLoadViewAndControllerEvent', Priority::NORMAL, $mockController); + + $this->router->defaultCallable($matches, '.*$'); + $this->assertEquals($mockController, $this->router->getCurrentController()); + } + /* route() ------------------------------------------------------------ */ + /** + * @depends testDefaultCallable + * @covers ::route + * @covers ::loadCallable + */ + public function testRoute() + { + // Add route first + $this->router->addRoute('(?P.*?)(|\/(?P.*?)(|\/(?P.*?)))(|\.(?P.*?))'); + // Create mock view and controller + $mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock(); + $mockView = $this->getMockBuilder('\FuzeWorks\View')->setMethods(['testMethod'])->getMock(); + class_alias(get_class($mockController), '\Application\Controller\TestRouteController'); + class_alias(get_class($mockView), '\Application\View\TestRouteTestView'); + + // Attempt to route + $this->assertnull($this->router->route('testRoute/testMethod/testParameters.test')); + $this->assertInstanceOf('\Application\Controller\TestRouteController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestRouteTestView', $this->router->getCurrentView()); + } + + /** + * @depends testRoute + * @covers ::route + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testRouteNotFound() + { + $this->router->route('NotFound'); + } + + /** + * @depends testRouteNotFound + * @covers ::route + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testRouteNotMatched() + { + $this->router->addRoute('NotMatched'); + $this->router->route('NotFound'); + } + + /** + * @depends testRoute + * @covers ::route + * @covers ::loadCallable + */ + public function testRouteStaticRewrite() + { + // Add route first + $this->router->addRoute( + 'staticRewrite', + [ + 'viewName' => 'TestStaticRewrite', + 'viewMethod' => 'someMethod', + 'viewType' => 'static' + ] + ); + + // Create mock view and controller + $mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock(); + $mockView = $this->getMockBuilder('\FuzeWorks\View')->setMethods(['someMethod'])->getMock(); + class_alias(get_class($mockController), '\Application\Controller\TestStaticRewriteController'); + class_alias(get_class($mockView), '\Application\View\TestStaticRewriteStaticView'); + + // Attempt to route + $this->assertnull($this->router->route('staticRewrite')); + $this->assertInstanceOf('\Application\Controller\TestStaticRewriteController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestStaticRewriteStaticView', $this->router->getCurrentView()); + } + + /** + * @depends testRouteStaticRewrite + * @covers ::route + * @covers ::loadCallable + */ + public function testRouteDynamicRewrite() + { + // Add route first + $this->router->addRoute( + 'dynamicRewrite', + function($matches){ + $this->assertEquals([0=>'dynamicRewrite'], $matches); + return [ + 'viewName' => 'TestDynamicRewrite', + 'viewMethod' => 'someMethod', + 'viewType' => 'static' + ]; + } + ); + + // Create mock view and controller + $mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock(); + $mockView = $this->getMockBuilder('\FuzeWorks\View')->setMethods(['someMethod'])->getMock(); + class_alias(get_class($mockController), '\Application\Controller\TestDynamicRewriteController'); + class_alias(get_class($mockView), '\Application\View\TestDynamicRewriteStaticView'); + + // Attempt to route + $this->assertNull($this->router->route('dynamicRewrite')); + $this->assertInstanceOf('\Application\Controller\TestDynamicRewriteController', $this->router->getCurrentController()); + $this->assertInstanceOf('\Application\View\TestDynamicRewriteStaticView', $this->router->getCurrentView()); + } + + /** + * @depends testRouteStaticRewrite + * @covers ::route + * @covers ::loadCallable + */ + public function testRouteCustomCallable() + { + // Create custom callable + $callable = function(array $matches, string $route){ + $this->assertEquals('customCallable', $route); + $this->assertEquals([0=>'customCallable'], $matches); + }; + + // Add route + $this->router->addRoute( + 'customCallable', + [ + 'callable' => $callable + ] + ); + + $this->assertNull($this->router->route('customCallable')); + } + + /** + * @depends testRouteStaticRewrite + * @covers ::route + * @covers ::loadCallable + * @expectedException \FuzeWorks\Exception\NotFoundException + */ + public function testRouteUnsatisfiedCallable() + { + // Create custom callable + $callable = function(array $matches, string $route){ + $this->assertEquals('unsatisfiedCallable', $route); + $this->assertEquals([0=>'unsatisfiedCallable'], $matches); + return false; + }; + + // Add route + $this->router->addRoute( + 'unsatisfiedCallable', + [ + 'callable' => $callable + ] + ); + + $this->assertNull($this->router->route('unsatisfiedCallable')); + } } diff --git a/test/phpunit.xml b/test/phpunit.xml index c758298..7708a45 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -27,7 +27,7 @@ ../ ../vendor/ - ../tests/ + ../test/ From d8acd801d243f1cc2e653b73bbcaedf669d332b9 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 11 Feb 2019 19:31:49 +0100 Subject: [PATCH 05/16] Implemented routerLoadCallableEvent Fixes #3 --- .../Event/RouterLoadCallableEvent.php | 82 +++++++++++++++++++ src/FuzeWorks/Router.php | 19 ++++- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/FuzeWorks/Event/RouterLoadCallableEvent.php diff --git a/src/FuzeWorks/Event/RouterLoadCallableEvent.php b/src/FuzeWorks/Event/RouterLoadCallableEvent.php new file mode 100644 index 0000000..f403b72 --- /dev/null +++ b/src/FuzeWorks/Event/RouterLoadCallableEvent.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class RouterLoadCallableEvent extends Event +{ + + /** + * The callable to be loaded + * + * @var callable + */ + public $callable; + + /** + * The matches with which the callable is loaded + * + * @var array + */ + public $matches; + + /** + * The route which resulted in this callable being loaded + * + * @var string + */ + public $route; + + public function init(callable $callable, array $matches, string $route) + { + $this->callable = $callable; + $this->matches = $matches; + $this->route = $route; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index e697ea3..37fbc01 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -36,6 +36,7 @@ namespace FuzeWorks; +use FuzeWorks\Event\RouterLoadCallableEvent; use FuzeWorks\Event\RouterLoadViewAndControllerEvent; use FuzeWorks\Exception\ConfigException; use FuzeWorks\Exception\ControllerException; @@ -187,6 +188,7 @@ class Router * @param string $path * @return mixed * @throws NotFoundException + * @throws RouterException */ public function route(string $path) { @@ -248,6 +250,7 @@ class Router * @param array $matches * @param string $route * @return mixed + * @throws RouterException */ protected function loadCallable(callable $callable, array $matches, string $route) { @@ -257,10 +260,22 @@ class Router if (!is_int($key)) Logger::log($key.': '.var_export($value, true).''); } - Logger::stopLevel(); + + try { + /** @var RouterLoadCallableEvent $event */ + $event = Events::fireEvent('routerLoadCallableEvent', + $callable, + $matches, + $route + ); + } catch (EventException $e) { + throw new RouterException("Could not load callable. routerLoadCallableEvent threw exception: '".$e->getMessage()."'"); + } // Invoke callable - return call_user_func_array($callable, [$matches, $route]); + $output = call_user_func_array($event->callable, [$event->matches, $event->route]); + Logger::stopLevel(); + return $output; } /** From 5b66dd5f29e3fef05fdbf144cdd7803cf02801f7 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 11 Feb 2019 19:47:24 +0100 Subject: [PATCH 06/16] Changed which properties Models, Views and Controllers contain. They no longer share the Factory. Fixes #5 --- src/FuzeWorks/Controller.php | 45 ++++++++++++++++++++++++++++++++++-- src/FuzeWorks/Model.php | 32 +++++++++++++++++++++++-- src/FuzeWorks/Router.php | 6 +++++ src/FuzeWorks/View.php | 32 +++++++++++++++++++++++-- 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/FuzeWorks/Controller.php b/src/FuzeWorks/Controller.php index 3d61683..4b1acef 100644 --- a/src/FuzeWorks/Controller.php +++ b/src/FuzeWorks/Controller.php @@ -39,11 +39,52 @@ namespace FuzeWorks; /** * Abstract class Controller. * - * Extends all controllers to use the Factory. + * Extends all controllers to use useful classes * * @author Abel Hoogeveen * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) */ -abstract class Controller extends Factory +abstract class Controller { + /** + * @var Plugins + */ + public $plugins; + + /** + * @var Libraries + */ + public $libraries; + + /** + * @var Helpers + */ + public $helpers; + + /** + * @var Config + */ + public $config; + + /** + * @var Controllers + */ + public $controllers; + + /** + * @var Models + */ + public $models; + + public function __construct() + { + $this->plugins = Factory::getInstance()->plugins; + $this->libraries = Factory::getInstance()->libraries; + $this->helpers = Factory::getInstance()->helpers; + $this->config = Factory::getInstance()->config; + $this->controllers = Factory::getInstance()->controllers; + $this->models = Factory::getInstance()->models; + } + + } \ No newline at end of file diff --git a/src/FuzeWorks/Model.php b/src/FuzeWorks/Model.php index 483cfd0..4c14fdd 100644 --- a/src/FuzeWorks/Model.php +++ b/src/FuzeWorks/Model.php @@ -40,11 +40,39 @@ namespace FuzeWorks; /** * Abstract class Model. * - * Extends all models to use the Factory. + * Extends all models to use useful classes * * @author Abel Hoogeveen * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) */ -abstract class Model extends Factory +abstract class Model { + /** + * @var Plugins + */ + public $plugins; + + /** + * @var Libraries + */ + public $libraries; + + /** + * @var Helpers + */ + public $helpers; + + /** + * @var Config + */ + public $config; + + public function __construct() + { + $this->plugins = Factory::getInstance()->plugins; + $this->libraries = Factory::getInstance()->libraries; + $this->helpers = Factory::getInstance()->helpers; + $this->config = Factory::getInstance()->config; + } + } diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index 37fbc01..0ac6b26 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -189,6 +189,7 @@ class Router * @return mixed * @throws NotFoundException * @throws RouterException + * @throws HaltException */ public function route(string $path) { @@ -251,6 +252,7 @@ class Router * @param string $route * @return mixed * @throws RouterException + * @throws HaltException */ protected function loadCallable(callable $callable, array $matches, string $route) { @@ -272,6 +274,10 @@ class Router throw new RouterException("Could not load callable. routerLoadCallableEvent threw exception: '".$e->getMessage()."'"); } + // Halt if cancelled + if ($event->isCancelled()) + throw new HaltException("Will not load callable. Cancelled by routerLoadCallableEvent."); + // Invoke callable $output = call_user_func_array($event->callable, [$event->matches, $event->route]); Logger::stopLevel(); diff --git a/src/FuzeWorks/View.php b/src/FuzeWorks/View.php index ccb5a81..f8d05a5 100644 --- a/src/FuzeWorks/View.php +++ b/src/FuzeWorks/View.php @@ -40,14 +40,34 @@ namespace FuzeWorks; /** * Abstract class View. * - * Extends all views to use the Factory. + * Extends all views to use useful classes * * @author Abel Hoogeveen * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) */ -abstract class View extends Factory +abstract class View { + /** + * @var Plugins + */ + public $plugins; + + /** + * @var Libraries + */ + public $libraries; + + /** + * @var Helpers + */ + public $helpers; + + /** + * @var Config + */ + public $config; + /** * The controller associated with this view * @@ -65,4 +85,12 @@ abstract class View extends Factory $this->controller = $controller; } + public function __construct() + { + $this->plugins = Factory::getInstance()->plugins; + $this->libraries = Factory::getInstance()->libraries; + $this->helpers = Factory::getInstance()->helpers; + $this->config = Factory::getInstance()->config; + } + } \ No newline at end of file From 4384ee23b10fb71b19f808dcfe396444b756e4e0 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 11 Feb 2019 19:56:25 +0100 Subject: [PATCH 07/16] Release release candidate 2 (In coordination with other FuzeWorks projects) --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7d28cef..00cffb5 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,9 @@ "php": ">=7.1.0" }, "require-dev": { - "fuzeworks/core": "dev-development", + "fuzeworks/core": "1.2.0-RC2", "phpunit/phpunit": "^7", - "mikey179/vfsStream": "1.6.5" + "mikey179/vfsstream": "1.6.5" }, "autoload": { "psr-4": { From f6baf1142c9a147f85fa5cbf6e7245d083c95c87 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 1 Mar 2019 11:04:12 +0100 Subject: [PATCH 08/16] Made all properties of the Model, View and Controller protected. These should not be accessible from outside these types of objects. Also made FuzeWorks core a requirement and not a dev-requirement. --- composer.json | 4 ++-- src/FuzeWorks/Controller.php | 12 ++++++------ src/FuzeWorks/Model.php | 8 ++++---- src/FuzeWorks/View.php | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 00cffb5..b7b476e 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,10 @@ } ], "require": { - "php": ">=7.1.0" + "php": ">=7.1.0", + "fuzeworks/core": "1.2.0-RC2" }, "require-dev": { - "fuzeworks/core": "1.2.0-RC2", "phpunit/phpunit": "^7", "mikey179/vfsstream": "1.6.5" }, diff --git a/src/FuzeWorks/Controller.php b/src/FuzeWorks/Controller.php index 4b1acef..f171162 100644 --- a/src/FuzeWorks/Controller.php +++ b/src/FuzeWorks/Controller.php @@ -49,32 +49,32 @@ abstract class Controller /** * @var Plugins */ - public $plugins; + protected $plugins; /** * @var Libraries */ - public $libraries; + protected $libraries; /** * @var Helpers */ - public $helpers; + protected $helpers; /** * @var Config */ - public $config; + protected $config; /** * @var Controllers */ - public $controllers; + protected $controllers; /** * @var Models */ - public $models; + protected $models; public function __construct() { diff --git a/src/FuzeWorks/Model.php b/src/FuzeWorks/Model.php index 4c14fdd..796bb60 100644 --- a/src/FuzeWorks/Model.php +++ b/src/FuzeWorks/Model.php @@ -50,22 +50,22 @@ abstract class Model /** * @var Plugins */ - public $plugins; + protected $plugins; /** * @var Libraries */ - public $libraries; + protected $libraries; /** * @var Helpers */ - public $helpers; + protected $helpers; /** * @var Config */ - public $config; + protected $config; public function __construct() { diff --git a/src/FuzeWorks/View.php b/src/FuzeWorks/View.php index f8d05a5..fa86c8c 100644 --- a/src/FuzeWorks/View.php +++ b/src/FuzeWorks/View.php @@ -51,22 +51,22 @@ abstract class View /** * @var Plugins */ - public $plugins; + protected $plugins; /** * @var Libraries */ - public $libraries; + protected $libraries; /** * @var Helpers */ - public $helpers; + protected $helpers; /** * @var Config */ - public $config; + protected $config; /** * The controller associated with this view From be414aa2cdb0e08864083d44767871a4522ca33a Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 1 Mar 2019 11:07:24 +0100 Subject: [PATCH 09/16] Implemented RouterCallViewEvent This event gets called right before defaultCallable() invokes the requested method on the requested view. This allows other classes to intervene when required. Currently used by the WebComponent to suppress CSRF Exceptions. --- src/FuzeWorks/Event/RouterCallViewEvent.php | 101 ++++++++++++++++++++ src/FuzeWorks/Router.php | 28 +++++- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/FuzeWorks/Event/RouterCallViewEvent.php diff --git a/src/FuzeWorks/Event/RouterCallViewEvent.php b/src/FuzeWorks/Event/RouterCallViewEvent.php new file mode 100644 index 0000000..47c1159 --- /dev/null +++ b/src/FuzeWorks/Event/RouterCallViewEvent.php @@ -0,0 +1,101 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class RouterCallViewEvent extends Event +{ + /** + * The function that will be loaded in the view + * + * @var string + */ + public $viewMethod; + + /** + * The parameters that will be provided to the function in the view + * + * @var string + */ + public $viewParameters; + + /** + * The route that resulted in this controller and view + * + * @var string + */ + public $route; + + /** + * The view the method will be called on + * + * @var View + */ + public $view; + + /** + * The controller that's associated with this View + * + * @var Controller + */ + public $controller; + + public function init(View $view, Controller $controller, string $viewMethod, string $viewParameters, string $route) + { + $this->view = $view; + $this->controller = $controller; + $this->viewMethod = $viewMethod; + $this->viewParameters = $viewParameters; + $this->route = $route; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index 0ac6b26..9ea7761 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -36,6 +36,7 @@ namespace FuzeWorks; +use FuzeWorks\Event\RouterCallViewEvent; use FuzeWorks\Event\RouterLoadCallableEvent; use FuzeWorks\Event\RouterLoadViewAndControllerEvent; use FuzeWorks\Exception\ConfigException; @@ -339,6 +340,28 @@ class Router return false; } + // Fire routerCallViewEvent + try { + /** @var RouterCallViewEvent $event */ + $event = Events::fireEvent('routerCallViewEvent', + $this->view, + $this->controller, + $event->viewMethod, + $event->viewParameters, + $event->route + ); + + // Reset vars + $this->view = $event->view; + $this->controller = $event->controller; + } catch (EventException $e) { + throw new RouterException("Could not load view. routerCallViewEvent threw exception: '".$e->getMessage()."'"); + } + + // Cancel if requested to do so + if ($event->isCancelled()) + throw new HaltException("Will not load view. Cancelled by routerCallViewEvent"); + // If the view does not want a function to be loaded, provide a halt parameter if (isset($this->view->halt)) throw new HaltException("Will not load view. Cancelled by 'halt' attribute in view."); @@ -347,7 +370,10 @@ class Router if (method_exists($this->view, $event->viewMethod) || method_exists($this->view, '__call')) { // Execute the function on the view - return $this->view->{$event->viewMethod}($event->viewParameters); + Logger::newLevel("Calling method '{$event->viewMethod}' on " . get_class($this->view) . ' with ' . get_class($this->controller)); + $output = $this->view->{$event->viewMethod}($event->viewParameters); + Logger::stopLevel(); + return $output; } else { From f49c5dd8826e8bdf40b94c3333854cf2295b9c73 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 4 Mar 2019 21:33:38 +0100 Subject: [PATCH 10/16] Implemented changes requested by FuzeWorks\Application - Added Priorities to the routes array. Routes can now be saved with priorities, making higher priority routes load before lower priorities - Fixed bug where callable is not reset upon attempting routing a new route after another was not satisfied --- src/FuzeWorks/Router.php | 124 ++++++++++++++++++++++----------------- test/mcr/RouterTest.php | 15 ++--- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index 9ea7761..ff6d77f 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -156,7 +156,14 @@ class Router } } - public function addRoute(string $route, $routeConfig = null, bool $prepend = true) + /** + * Add a route to the Router + * + * @param string $route + * @param null $routeConfig + * @param int $priority + */ + public function addRoute(string $route, $routeConfig = null, int $priority = Priority::NORMAL) { // Set defaultCallable if no value provided if (is_null($routeConfig)) @@ -165,23 +172,27 @@ class Router // Convert wildcards to Regex $route = str_replace([':any',':num'], ['[^/]+', '[0-9]+'], $route); - if ($prepend) - $this->routes = [$route => $routeConfig] + $this->routes; - else - $this->routes[$route] = $routeConfig; + if (!isset($this->routes[$priority])) + $this->routes[$priority] = []; - Logger::log('Route added at '.($prepend ? 'top' : 'bottom').': "'.$route.'"'); + if (!isset($this->routes[$priority][$route])) + $this->routes[$priority][$route] = $routeConfig; + + Logger::log('Route added with ' . Priority::getPriority($priority) . ": '" . $route."'"); } /** * Removes a route from the array based on the given route. * * @param $route string The route to remove + * @param int $priority */ - public function removeRoute(string $route) + public function removeRoute(string $route, int $priority = Priority::NORMAL) { - unset($this->routes[$route]); + if (!isset($this->routes[$priority][$route])) + return; + unset($this->routes[$priority][$route]); Logger::log('Route removed: '.$route); } @@ -194,54 +205,60 @@ class Router */ public function route(string $path) { - // Check all the provided custom paths - foreach ($this->routes as $route => $routeConfig) - { - // Match the path against the routes - if (!preg_match('#^'.$route.'$#', $path, $matches)) + // Check all the provided custom paths, ordered by priority + for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) { + if (!isset($this->routes[$i])) continue; - // Save the matches - Logger::log('Route matched: '.$route); - $this->matches = $matches; - $this->route = $route; - - // Call callable if routeConfig is callable, so routeConfig can be replaced - // This is an example of 'Dynamic Rewrite' - // e.g: '.*$' => callable - if (is_callable($routeConfig)) - $routeConfig = call_user_func_array($routeConfig, [$matches]); - - // If routeConfig is an array, multiple things might be at hand - if (is_array($routeConfig)) + foreach ($this->routes[$i] as $route => $routeConfig) { - // Replace defaultCallable if a custom callable is provided - // This is an example of 'Custom Callable' - // e.g: '.*$' => ['callable' => [$object, 'method']] - if (isset($routeConfig['callable']) && is_callable($routeConfig['callable'])) - $this->callable = $routeConfig['callable']; + // Match the path against the routes + if (!preg_match('#^'.$route.'$#', $path, $matches)) + continue; - // If the route provides a configuration, use that - // This is an example of 'Static Rewrite' - // e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index'] - else - $this->matches = array_merge($this->matches, $routeConfig); + // Save the matches + Logger::log("Route matched: '" . $route . "' with " . Priority::getPriority($i)); + $this->matches = $matches; + $this->route = $route; + $this->callable = null; + + // Call callable if routeConfig is callable, so routeConfig can be replaced + // This is an example of 'Dynamic Rewrite' + // e.g: '.*$' => callable + if (is_callable($routeConfig)) + $routeConfig = call_user_func_array($routeConfig, [$matches]); + + // If routeConfig is an array, multiple things might be at hand + if (is_array($routeConfig)) + { + // Replace defaultCallable if a custom callable is provided + // This is an example of 'Custom Callable' + // e.g: '.*$' => ['callable' => [$object, 'method']] + if (isset($routeConfig['callable']) && is_callable($routeConfig['callable'])) + $this->callable = $routeConfig['callable']; + + // If the route provides a configuration, use that + // This is an example of 'Static Rewrite' + // e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index'] + else + $this->matches = array_merge($this->matches, $routeConfig); + } + + // If no custom callable is provided, use default + // This is an example of 'Default Callable' + if (is_null($this->callable)) + $this->callable = [$this, 'defaultCallable']; + + // Attempt and load callable. If false, continue + $output = $this->loadCallable($this->callable, $this->matches, $route); + if (is_bool($output) && $output === FALSE) + { + Logger::log('Callable not satisfied, skipping to next callable'); + continue; + } + + return $output; } - - // If no custom callable is provided, use default - // This is an example of 'Default Callable' - if (is_null($this->callable)) - $this->callable = [$this, 'defaultCallable']; - - // Attempt and load callable. If false, continue - $output = $this->loadCallable($this->callable, $this->matches, $route); - if (is_bool($output) && $output === FALSE) - { - Logger::log('Callable not satisfied, skipping to next callable'); - continue; - } - - return $output; } throw new NotFoundException("Could not load view. Router could not find matching route with satisfied callable."); @@ -387,12 +404,13 @@ class Router /** * Returns an array with all the routes. * + * @param int $priority * @return array * @codeCoverageIgnore */ - public function getRoutes(): array + public function getRoutes(int $priority = Priority::NORMAL): array { - return $this->routes; + return $this->routes[$priority]; } /** diff --git a/test/mcr/RouterTest.php b/test/mcr/RouterTest.php index cb6641f..a57f5ea 100644 --- a/test/mcr/RouterTest.php +++ b/test/mcr/RouterTest.php @@ -120,19 +120,14 @@ class RouterTest extends MVCRTestAbstract $testAppendRouteFunction = [function () { }]; $this->router->addRoute('testRoute', $testRouteFunction); - $this->router->addRoute('testAppendRoute', $testAppendRouteFunction, false); + $this->router->addRoute('testAppendRoute', $testAppendRouteFunction, Priority::LOW); // Test if the order is correct - $this->assertSame( - ['testRoute' => $testRouteFunction, 'testAppendRoute' => $testAppendRouteFunction], - $this->router->getRoutes() - ); + // First for Priority::NORMAL + $this->assertSame(['testRoute' => $testRouteFunction], $this->router->getRoutes(Priority::NORMAL)); - // Test if the order is not incorrect - $this->assertNotSame( - ['testAppendRoute' => $testAppendRouteFunction, 'testRoute' => $testRouteFunction], - $this->router->getRoutes() - ); + // Then for Priority::LOW + $this->assertSame(['testAppendRoute' => $testAppendRouteFunction], $this->router->getRoutes(Priority::LOW)); } /** From f15c971ace6e3ca676fa52efbe811ef371fd8dab Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 4 Mar 2019 21:42:35 +0100 Subject: [PATCH 11/16] Release 1.2.0-RC3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b7b476e..2aca775 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/core": "1.2.0-RC2" + "fuzeworks/core": "1.2.0-RC3" }, "require-dev": { "phpunit/phpunit": "^7", From af476c6f3e7d5eecb5de38fb0b0a8c9f8b23bfb0 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 22 Jul 2019 11:46:43 +0200 Subject: [PATCH 12/16] Updated some comments to reflect its functionality better --- src/FuzeWorks/Views.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FuzeWorks/Views.php b/src/FuzeWorks/Views.php index 1a77ce7..b38c968 100644 --- a/src/FuzeWorks/Views.php +++ b/src/FuzeWorks/Views.php @@ -83,7 +83,7 @@ class Views else $arguments = []; - // Fire a view load event + // Fire a viewGetEvent /** @var ViewGetEvent $event */ try { $event = Events::fireEvent('viewGetEvent', $viewName, $viewType, $viewPaths, $namespace, $controller, $arguments); From c7600ffcd7c54c70877336b69c0f7dbc70219d28 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Wed, 21 Aug 2019 18:57:40 +0200 Subject: [PATCH 13/16] Release 1.2.0-RC4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2aca775..5fa6bac 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/core": "1.2.0-RC3" + "fuzeworks/core": "1.2.0-RC4" }, "require-dev": { "phpunit/phpunit": "^7", From ed90b3badb10597f5ae17a72a96af5af1cdcbbfe Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Tue, 17 Sep 2019 17:24:12 +0200 Subject: [PATCH 14/16] Implemented the usage of multiple viewMethods. - This change allows the developer to sort methods with the usual Priority. The first valid method found shall be used. - Dependency for implementing methods in a REST API. --- src/FuzeWorks/Event/RouterCallViewEvent.php | 24 +++++++-- .../RouterLoadViewAndControllerEvent.php | 24 +++++++-- src/FuzeWorks/Router.php | 37 +++++++------ test/mcr/RouterTest.php | 28 +++++++++- ...w.test.testdefaultcallablechangemethod.php | 52 +++++++++++++++++++ 5 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 test/views/view.test.testdefaultcallablechangemethod.php diff --git a/src/FuzeWorks/Event/RouterCallViewEvent.php b/src/FuzeWorks/Event/RouterCallViewEvent.php index 47c1159..8cb6679 100644 --- a/src/FuzeWorks/Event/RouterCallViewEvent.php +++ b/src/FuzeWorks/Event/RouterCallViewEvent.php @@ -38,6 +38,7 @@ namespace FuzeWorks\Event; use FuzeWorks\Controller; use FuzeWorks\Event; +use FuzeWorks\Priority; use FuzeWorks\View; /** @@ -58,9 +59,9 @@ class RouterCallViewEvent extends Event /** * The function that will be loaded in the view * - * @var string + * @var array */ - public $viewMethod; + public $viewMethods; /** * The parameters that will be provided to the function in the view @@ -90,12 +91,27 @@ class RouterCallViewEvent extends Event */ public $controller; - public function init(View $view, Controller $controller, string $viewMethod, string $viewParameters, string $route) + public function init(View $view, Controller $controller, array $viewMethods, string $viewParameters, string $route) { $this->view = $view; $this->controller = $controller; - $this->viewMethod = $viewMethod; + $this->viewMethods = $viewMethods; $this->viewParameters = $viewParameters; $this->route = $route; } + + /** + * Add a method which should be tried upon calling the view + * + * @param string $method + * @param int $priority + */ + public function addMethod(string $method, int $priority = Priority::NORMAL) + { + if (!isset($this->viewMethods[$priority])) + $this->viewMethods[$priority] = []; + + if (!isset($this->viewMethods[$priority][$method])) + $this->viewMethods[$priority][] = $method; + } } \ No newline at end of file diff --git a/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php index d072c33..dcb9c82 100644 --- a/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php +++ b/src/FuzeWorks/Event/RouterLoadViewAndControllerEvent.php @@ -37,6 +37,7 @@ namespace FuzeWorks\Event; use FuzeWorks\Controller; use FuzeWorks\Event; +use FuzeWorks\Priority; /** * Event that gets fired when a view and controller are loaded. @@ -67,9 +68,9 @@ class RouterLoadViewAndControllerEvent extends Event /** * The function that will be loaded in the view * - * @var string + * @var array */ - public $viewMethod; + public $viewMethods; /** * The parameters that will be provided to the function in the view @@ -92,15 +93,30 @@ class RouterLoadViewAndControllerEvent extends Event */ public $controller; - public function init(string $viewName, string $viewType, string $viewMethod, string $viewParameters, string $route) + public function init(string $viewName, string $viewType, array $viewMethods, string $viewParameters, string $route) { $this->viewName = $viewName; $this->viewType = $viewType; - $this->viewMethod = $viewMethod; + $this->viewMethods = $viewMethods; $this->viewParameters = $viewParameters; $this->route = $route; } + /** + * Add a method which should be tried upon calling the view + * + * @param string $method + * @param int $priority + */ + public function addMethod(string $method, int $priority = Priority::NORMAL) + { + if (!isset($this->viewMethods[$priority])) + $this->viewMethods[$priority] = []; + + if (!isset($this->viewMethods[$priority][$method])) + $this->viewMethods[$priority][] = $method; + } + /** * Override the controller to be provided to the view. * diff --git a/src/FuzeWorks/Router.php b/src/FuzeWorks/Router.php index ff6d77f..cc32824 100644 --- a/src/FuzeWorks/Router.php +++ b/src/FuzeWorks/Router.php @@ -325,7 +325,8 @@ class Router $event = Events::fireEvent('routerLoadViewAndControllerEvent', $viewName, $viewType, - $viewMethod, + // ViewMethod is provided as a Priority::NORMAL method + [3 => [$viewMethod]], $viewParameters, $route ); @@ -363,7 +364,7 @@ class Router $event = Events::fireEvent('routerCallViewEvent', $this->view, $this->controller, - $event->viewMethod, + $event->viewMethods, $event->viewParameters, $event->route ); @@ -383,21 +384,27 @@ class Router if (isset($this->view->halt)) throw new HaltException("Will not load view. Cancelled by 'halt' attribute in view."); - // Check if requested function or magic method exists in view - if (method_exists($this->view, $event->viewMethod) || method_exists($this->view, '__call')) - { - // Execute the function on the view - Logger::newLevel("Calling method '{$event->viewMethod}' on " . get_class($this->view) . ' with ' . get_class($this->controller)); - $output = $this->view->{$event->viewMethod}($event->viewParameters); - Logger::stopLevel(); - return $output; - } - else - { - Logger::logError("Could not load view. View does not have method '".$event->viewMethod."'"); + // Cycle over every viewMethod until a valid one is found + for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) { + if (!isset($event->viewMethods[$i])) + continue; + + foreach ($event->viewMethods[$i] as $method) { + if (method_exists($this->view, $method)) + { + // Execute this method on the view + Logger::newLevel("Calling method '{$method}' on " . get_class($this->view) . ' with ' . get_class($this->controller)); + $output = $this->view->{$method}($event->viewParameters); + Logger::stopLevel(); + return $output; + } + } } - // View could not be found + // Otherwise log an error + Logger::logError("Could not load view. View does not have any of the provided methods."); + + // View could not be found. return false; } diff --git a/test/mcr/RouterTest.php b/test/mcr/RouterTest.php index a57f5ea..de9fe7a 100644 --- a/test/mcr/RouterTest.php +++ b/test/mcr/RouterTest.php @@ -331,7 +331,7 @@ class RouterTest extends MVCRTestAbstract $this->assertInstanceOf('\FuzeWorks\Event\RouterLoadViewAndControllerEvent', $event); $this->assertEquals('TestDefaultCallable', $event->viewName); $this->assertEquals('test', $event->viewType); - $this->assertEquals('missing', $event->viewMethod); + $this->assertEquals([3=>['missing']], $event->viewMethods); $event->setCancelled(true); }, 'routerLoadViewAndControllerEvent'); @@ -365,6 +365,32 @@ class RouterTest extends MVCRTestAbstract $this->assertEquals($mockController, $this->router->getCurrentController()); } + /** + * @depends testDefaultCallableReplaceController + * @covers ::defaultCallable + * @covers \FuzeWorks\Event\RouterLoadViewAndControllerEvent::addMethod + */ + public function testDefaultCallableAddMethod() + { + $matches = [ + 'viewName' => 'TestDefaultCallableChangeMethod', + 'viewType' => 'test', + 'viewMethod' => 'index' + ]; + + $this->assertNull($this->router->getCurrentController()); + $this->assertNull($this->router->getCurrentView()); + + $mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock(); + // Create listener + Events::addListener(function($event, $mockController){ + $event->overrideController($mockController); + $event->addMethod('altered', Priority::HIGH); + }, 'routerLoadViewAndControllerEvent', Priority::NORMAL, $mockController); + + $this->assertEquals('Altered!', $this->router->defaultCallable($matches, '.*$')); + } + /* route() ------------------------------------------------------------ */ /** diff --git a/test/views/view.test.testdefaultcallablechangemethod.php b/test/views/view.test.testdefaultcallablechangemethod.php new file mode 100644 index 0000000..3533d0e --- /dev/null +++ b/test/views/view.test.testdefaultcallablechangemethod.php @@ -0,0 +1,52 @@ + Date: Wed, 18 Sep 2019 00:02:18 +0200 Subject: [PATCH 15/16] Release version 1.2.0-RC5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5fa6bac..eac9c27 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/core": "1.2.0-RC4" + "fuzeworks/core": "1.2.0-RC5" }, "require-dev": { "phpunit/phpunit": "^7", From fdf998c37b41a02446286fa62516dc028fd47fa7 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Sat, 21 Sep 2019 16:37:21 +0200 Subject: [PATCH 16/16] Release version 1.2.0 --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index eac9c27..0e44c9f 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/core": "1.2.0-RC5" + "fuzeworks/core": "~1.2.0" }, "require-dev": { "phpunit/phpunit": "^7", @@ -25,5 +25,4 @@ "FuzeWorks\\": "src/FuzeWorks/" } } - } \ No newline at end of file