listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'handleRouteNotFoundError']); } /** * Set flag indicating whether or not to display the routing failure * * @param bool $displayNotFoundReason * @return RouteNotFoundStrategy */ public function setDisplayNotFoundReason($displayNotFoundReason) { $this->displayNotFoundReason = (bool) $displayNotFoundReason; return $this; } /** * Do we display the routing failure? * * @return bool */ public function displayNotFoundReason() { return $this->displayNotFoundReason; } /** * Detect if an error is a route not found condition * * If a "controller not found" or "invalid controller" error type is * encountered, sets the response status code to 404. * * @param MvcEvent $e * @throws RuntimeException * @throws ServiceNotFoundException * @return void */ public function handleRouteNotFoundError(MvcEvent $e) { $error = $e->getError(); if (empty($error)) { return; } $response = $e->getResponse(); $request = $e->getRequest(); switch ($error) { case Application::ERROR_CONTROLLER_NOT_FOUND: case Application::ERROR_CONTROLLER_INVALID: case Application::ERROR_ROUTER_NO_MATCH: $this->reason = $error; if (! $response) { $response = new ConsoleResponse(); $e->setResponse($response); } $response->setMetadata('error', $error); break; default: return; } $result = $e->getResult(); if ($result instanceof Response) { // Already have a response as the result return; } // Prepare Console View Model $model = new ConsoleModel(); $model->setErrorLevel(1); // Fetch service manager $sm = $e->getApplication()->getServiceManager(); // Try to fetch module manager $mm = null; try { $mm = $sm->get('ModuleManager'); } catch (ServiceNotFoundException $exception) { // The application does not have or use module manager, so we cannot use it } // Try to fetch current console adapter try { $console = $sm->get('console'); if (! $console instanceof ConsoleAdapter) { throw new ServiceNotFoundException(); } } catch (ServiceNotFoundException $exception) { // The application does not have console adapter throw new RuntimeException('Cannot access Console adapter - is it defined in ServiceManager?'); } // Retrieve the script's name (entry point) $scriptName = ''; if ($request instanceof ConsoleRequest) { $scriptName = basename($request->getScriptName()); } // Get application banner $banner = $this->getConsoleBanner($console, $mm); // Get application usage information $usage = $this->getConsoleUsage($console, $scriptName, $mm); // Inject the text into view $result = $banner ? rtrim($banner, "\r\n") : ''; $result .= $usage ? "\n\n" . trim($usage, "\r\n") : ''; $result .= "\n"; // to ensure we output a final newline $result .= $this->reportNotFoundReason($e); $model->setResult($result); // Inject the result into MvcEvent $e->setResult($model); } /** * Build Console application banner text by querying currently loaded * modules. * * @param ModuleManagerInterface $moduleManager * @param ConsoleAdapter $console * @return string */ protected function getConsoleBanner(ConsoleAdapter $console, ModuleManagerInterface $moduleManager = null) { /* * Loop through all loaded modules and collect banners */ $banners = []; if ($moduleManager !== null) { foreach ($moduleManager->getLoadedModules(false) as $module) { // Strict-type on ConsoleBannerProviderInterface, or duck-type // on the method it defines if (! $module instanceof ConsoleBannerProviderInterface && ! method_exists($module, 'getConsoleBanner') ) { continue; // this module does not provide a banner } // Don't render empty completely empty lines $banner = $module->getConsoleBanner($console); if ($banner == '') { continue; } // We colorize each banners in blue for visual emphasis $banners[] = $console->colorize($banner, ColorInterface::BLUE); } } /* * Handle an application with no defined banners */ if (! count($banners)) { return "Zend Framework application\nUsage:\n"; } /* * Join the banners by a newline character */ return implode("\n", $banners); } /** * Build Console usage information by querying currently loaded modules. * * @param ConsoleAdapter $console * @param string $scriptName * @param ModuleManagerInterface $moduleManager * @return string * @throws RuntimeException */ protected function getConsoleUsage( ConsoleAdapter $console, $scriptName, ModuleManagerInterface $moduleManager = null ) { /* * Loop through all loaded modules and collect usage info */ $usageInfo = []; if ($moduleManager !== null) { foreach ($moduleManager->getLoadedModules(false) as $name => $module) { // Strict-type on ConsoleUsageProviderInterface, or duck-type // on the method it defines if (! $module instanceof ConsoleUsageProviderInterface && ! method_exists($module, 'getConsoleUsage') ) { continue; // this module does not provide usage info } // We prepend the usage by the module name (printed in red), so that each module is // clearly visible by the user $moduleName = sprintf( "%s\n%s\n%s\n", str_repeat('-', $console->getWidth()), $name, str_repeat('-', $console->getWidth()) ); $moduleName = $console->colorize($moduleName, ColorInterface::RED); $usage = $module->getConsoleUsage($console); // Normalize what we got from the module or discard if (is_array($usage) && ! empty($usage)) { array_unshift($usage, $moduleName); $usageInfo[$name] = $usage; } elseif (is_string($usage) && ($usage !== '')) { $usageInfo[$name] = [$moduleName, $usage]; } } } /* * Handle an application with no usage information */ if (! count($usageInfo)) { // TODO: implement fetching available console routes from router return ''; } /* * Transform arrays in usage info into columns, otherwise join everything together */ $result = ''; $table = false; $tableCols = 0; $tableType = 0; foreach ($usageInfo as $moduleName => $usage) { if (! is_string($usage) && ! is_array($usage)) { throw new RuntimeException(sprintf( 'Cannot understand usage info for module "%s"', $moduleName )); } if (is_string($usage)) { // It's a plain string - output as is $result .= $usage . "\n"; continue; } // It's an array, analyze it foreach ($usage as $a => $b) { /* * 'invocation method' => 'explanation' */ if (is_string($a) && is_string($b)) { if (($tableCols !== 2 || $tableType !== 1) && $table !== false) { // render last table $result .= $this->renderTable($table, $tableCols, $console->getWidth()); $table = false; // add extra newline for clarity $result .= "\n"; } // Colorize the command $a = $console->colorize($scriptName . ' ' . $a, ColorInterface::GREEN); $tableCols = 2; $tableType = 1; $table[] = [$a, $b]; continue; } /* * array('--param', '--explanation') */ if (is_array($b)) { if ((count($b) !== $tableCols || $tableType !== 2) && $table !== false) { // render last table $result .= $this->renderTable($table, $tableCols, $console->getWidth()); $table = false; // add extra newline for clarity $result .= "\n"; } $tableCols = count($b); $tableType = 2; $table[] = $b; continue; } /* * 'A single line of text' */ if ($table !== false) { // render last table $result .= $this->renderTable($table, $tableCols, $console->getWidth()); $table = false; // add extra newline for clarity $result .= "\n"; } $tableType = 0; $result .= $b . "\n"; } } // Finish last table if ($table !== false) { $result .= $this->renderTable($table, $tableCols, $console->getWidth()); } return $result; } /** * Render a text table containing the data provided, that will fit inside console window's width. * * @param $data * @param $cols * @param $consoleWidth * @return string */ protected function renderTable($data, $cols, $consoleWidth) { $result = ''; $padding = 2; // If there is only 1 column, just concatenate it if ($cols == 1) { foreach ($data as $row) { if (! isset($row[0])) { continue; } $result .= $row[0] . "\n"; } return $result; } // Get the string wrapper supporting UTF-8 character encoding $strWrapper = StringUtils::getWrapper('UTF-8'); // Determine max width for each column $maxW = []; for ($x = 1; $x <= $cols; $x += 1) { $maxW[$x] = 0; foreach ($data as $row) { $maxW[$x] = max($maxW[$x], $strWrapper->strlen($row[$x - 1]) + $padding * 2); } } /* * Check if the sum of x-1 columns fit inside console window width - 10 * chars. If columns do not fit inside console window, then we'll just * concatenate them and output as is. */ $width = 0; for ($x = 1; $x < $cols; $x += 1) { $width += $maxW[$x]; } if ($width >= $consoleWidth - 10) { foreach ($data as $row) { $result .= implode(" ", $row) . "\n"; } return $result; } /* * Use Zend\Text\Table to render the table. * The last column will use the remaining space in console window * (minus 1 character to prevent double wrapping at the edge of the * screen). */ $maxW[$cols] = $consoleWidth - $width - 1; $table = new Table\Table(); $table->setColumnWidths($maxW); $table->setDecorator(new Table\Decorator\Blank()); $table->setPadding(2); foreach ($data as $row) { $table->appendRow($row); } return $table->render(); } /** * Report the 404 reason and/or exceptions * * @param \Zend\EventManager\EventInterface $e * @return string */ protected function reportNotFoundReason($e) { if (! $this->displayNotFoundReason()) { return ''; } $exception = $e->getParam('exception', false); if (! $exception && ! $this->reason) { return ''; } $reason = ! empty($this->reason) ? $this->reason : 'unknown'; $reasons = [ Application::ERROR_CONTROLLER_NOT_FOUND => 'Could not match to a controller', Application::ERROR_CONTROLLER_INVALID => 'Invalid controller specified', Application::ERROR_ROUTER_NO_MATCH => 'Invalid arguments or no arguments provided', 'unknown' => 'Unknown', ]; $report = sprintf("\nReason for failure: %s\n", $reasons[$reason]); // @todo clean up once PHP 7 requirement is enforced while ($exception instanceof \Exception || $exception instanceof \Throwable) { $report .= sprintf( "Exception: %s\nTrace:\n%s\n", $exception->getMessage(), $exception->getTraceAsString() ); $exception = $exception->getPrevious(); } return $report; } }