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 * @throws RouterException * @throws HaltException */ 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 // This is an example of 'Dynamic Rewrite' // e.g: '.*$' => callable if (is_callable($routeConfig)) $routeConfig = call_user_func_array($routeConfig, [$matches]); // If routeConfig is an array, multiple things might be at hand if (is_array($routeConfig)) { // Replace defaultCallable if a custom callable is provided // This is an example of 'Custom Callable' // e.g: '.*$' => ['callable' => [$object, 'method']] if (isset($routeConfig['callable']) && is_callable($routeConfig['callable'])) $this->callable = $routeConfig['callable']; // If the route provides a configuration, use that // This is an example of 'Static Rewrite' // e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index'] else $this->matches = array_merge($this->matches, $routeConfig); } // If no custom callable is provided, use default // This is an example of 'Default Callable' if (is_null($this->callable)) $this->callable = [$this, 'defaultCallable']; // Attempt and load callable. If false, continue $output = $this->loadCallable($this->callable, $this->matches, $route); if (is_bool($output) && $output === FALSE) { Logger::log('Callable not satisfied, skipping to next callable'); continue; } return $output; } throw new NotFoundException("Could not load view. Router could not find matching route with satisfied callable."); } /** * @param callable $callable * @param array $matches * @param string $route * @return mixed * @throws RouterException * @throws HaltException */ 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).''); } try { /** @var RouterLoadCallableEvent $event */ $event = Events::fireEvent('routerLoadCallableEvent', $callable, $matches, $route ); } catch (EventException $e) { throw new RouterException("Could not load callable. routerLoadCallableEvent threw exception: '".$e->getMessage()."'"); } // Halt if cancelled if ($event->isCancelled()) throw new HaltException("Will not load callable. Cancelled by routerLoadCallableEvent."); // Invoke callable $output = call_user_func_array($event->callable, [$event->matches, $event->route]); Logger::stopLevel(); return $output; } /** * @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 = !empty($matches['viewName']) ? $matches['viewName'] : $this->config->routing->default_view; $viewType = !empty($matches['viewType']) ? $matches['viewType'] : $this->config->routing->default_viewType; $viewMethod = !empty($matches['viewMethod']) ? $matches['viewMethod'] : $this->config->routing->default_viewMethod; $viewParameters = !empty($matches['viewParameters']) ? $matches['viewParameters'] : ''; try { /** @var RouterLoadViewAndControllerEvent $event */ $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) { Logger::logError("Could not load view. Controller does not exist."); 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) { Logger::logError("Could not load view. View does not exist."); return false; } // Fire routerCallViewEvent try { /** @var RouterCallViewEvent $event */ $event = Events::fireEvent('routerCallViewEvent', $this->view, $this->controller, $event->viewMethod, $event->viewParameters, $event->route ); // Reset vars $this->view = $event->view; $this->controller = $event->controller; } catch (EventException $e) { throw new RouterException("Could not load view. routerCallViewEvent threw exception: '".$e->getMessage()."'"); } // Cancel if requested to do so if ($event->isCancelled()) throw new HaltException("Will not load view. Cancelled by routerCallViewEvent"); // If the view does not want a function to be loaded, provide a halt parameter if (isset($this->view->halt)) throw new HaltException("Will not load view. Cancelled by 'halt' attribute in view."); // Check if requested function or magic method exists in view if (method_exists($this->view, $event->viewMethod) || method_exists($this->view, '__call')) { // Execute the function on the view Logger::newLevel("Calling method '{$event->viewMethod}' on " . get_class($this->view) . ' with ' . get_class($this->controller)); $output = $this->view->{$event->viewMethod}($event->viewParameters); Logger::stopLevel(); return $output; } else { Logger::logError("Could not load view. View does not have method '".$event->viewMethod."'"); } // 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; } }