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 @@
+