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/.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/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..3f4a2e3 --- /dev/null +++ b/src/Config/config.routes.php @@ -0,0 +1,58 @@ + 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( +); diff --git a/src/Config/config.routing.php b/src/Config/config.routing.php new file mode 100644 index 0000000..a339f4f --- /dev/null +++ b/src/Config/config.routing.php @@ -0,0 +1,43 @@ + '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/temp/placeholder b/test/temp/placeholder new file mode 100644 index 0000000..e69de29 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 @@ +