diff --git a/CHANGELOG.md b/CHANGELOG.md index 2842a7b372cde7d7f44356c6b5cb74f0a35d1a4b..8b61c4181fcbd2525447d1e3e6b7649f86c90b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,110 +1,6 @@ -# Changelog +CHANGELOG +========= -All notable changes to this project will be documented in this file, in reverse chronological order by release. - -## 2.8.1 - TBD - -### Added - -- Nothing. - -### Changed - -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- Nothing. - -### Fixed - -- Nothing. - -## 2.8.0 - 2019-02-04 - -### Added - -- [zendframework/zend-console#41](https://github.com/zendframework/zend-console/pull/41) adds support for PHP 7.3. - -### Changed - -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- [zendframework/zend-console#41](https://github.com/zendframework/zend-console/pull/41) removes support for laminas-stdlib v2 releases. - -### Fixed - -- [zendframework/zend-console#44](https://github.com/zendframework/zend-console/pull/44) fixes usage of `array_unique()` within the `DefaultRouteMatcher` to - properly re-assign the array when invoked. - -## 2.7.0 - 2018-01-25 - -### Added - -- [zendframework/zend-console#32](https://github.com/zendframework/zend-console/pull/32) adds a new route - match type, the "catch-all". Such types are always optional (thus, appear in - `[]` sets), and are specified using `...` within: `command [...options]`. - - Parameters matched this way will always be returned as an array of values. - -- [zendframework/zend-console#39](https://github.com/zendframework/zend-console/pull/39) adds support for - PHP 7.2. - -### Changed - -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- [zendframework/zend-console#39](https://github.com/zendframework/zend-console/pull/39) removes support - for PHP 5.5. - -- [zendframework/zend-console#39](https://github.com/zendframework/zend-console/pull/39) removes support - for HHVM. - -### Fixed - -- [zendframework/zend-console#19](https://github.com/zendframework/zend-console/pull/19) updated link - to the documentation in the [README](README.md) - -## 2.6.0 - 2016-02-9 - -### Added - -- [zendframework/zend-console#16](https://github.com/zendframework/zend-console/pull/16) updates, - reorganizes, and publishes the documentation to - https://docs.laminas.dev/laminas-console - -### Deprecated - -- Nothing. - -### Removed - -- Nothing. - -### Fixed - -- [zendframework/zend-console#13](https://github.com/zendframework/zend-console/pull/13) updates the - component to make it forwards-compatible with the laminas-stdlib and - laminas-servicemanager v3 versions. -- [zendframework/zend-console#4](https://github.com/zendframework/zend-console/pull/4) fixes an error in - `getTitle()` whereby the `$output` array was being incorrectly used as a - string. -- [zendframework/zend-console#12](https://github.com/zendframework/zend-console/pull/12) updates the - `Laminas\Console\Prompt\Char::show()` method to call on the composed adapter's - `write()`/`writeLine()` methods instead of calling `echo()`. +1.0.0 +----- +- Reprise et fusion du code de Laminas-console et laminas-mvc-console \ No newline at end of file diff --git a/COPYRIGHT.md b/COPYRIGHT.md deleted file mode 100644 index 0a8cccc06bfa04935c37edde9b9923507da6126b..0000000000000000000000000000000000000000 --- a/COPYRIGHT.md +++ /dev/null @@ -1 +0,0 @@ -Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 10b40f1423b53b3138f9eb7db2db0698ba102c9a..0000000000000000000000000000000000000000 --- a/LICENSE.md +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -- Neither the name of Laminas Foundation nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f5492b5c1ec0e39c82636100969eda67a658d1b9 --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,94 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console; + +use Laminas\Mvc\SendResponseListener; +use Laminas\Router\RouteStackInterface; +use Laminas\ServiceManager\Factory\InvokableFactory; + +class ConfigProvider +{ + /** + * Provide configuration for this component. + * + * @return array + */ + public function __invoke() + { + return [ + 'controller_plugins' => $this->getPluginConfig(), + 'dependencies' => $this->getDependencyConfig(), + ]; + } + + /** + * Provide dependency configuration for this component. + * + * @return array + */ + public function getDependencyConfig() + { + return [ + 'aliases' => [ + 'console' => 'ConsoleAdapter', + 'Console' => 'ConsoleAdapter', + 'ConsoleDefaultRenderingStrategy' => View\DefaultRenderingStrategy::class, + 'ConsoleRenderer' => View\Renderer::class, + + // Legacy Zend Framework aliases + \Zend\Mvc\Console\View\DefaultRenderingStrategy::class => View\DefaultRenderingStrategy::class, + \Zend\Mvc\Console\View\Renderer::class => View\Renderer::class, + ], + 'delegators' => [ + 'ControllerManager' => [ Service\ControllerManagerDelegatorFactory::class ], + 'Request' => [ Service\ConsoleRequestDelegatorFactory::class ], + 'Response' => [ Service\ConsoleResponseDelegatorFactory::class ], + RouteStackInterface::class => [ Router\ConsoleRouterDelegatorFactory::class ], + SendResponseListener::class => [ Service\ConsoleResponseSenderDelegatorFactory::class ], + 'ViewHelperManager' => [ Service\ConsoleViewHelperManagerDelegatorFactory::class ], + 'ViewManager' => [ Service\ViewManagerDelegatorFactory::class ], + ], + 'factories' => [ + 'ConsoleAdapter' => Service\ConsoleAdapterFactory::class, + 'ConsoleExceptionStrategy' => Service\ConsoleExceptionStrategyFactory::class, + 'ConsoleRouteNotFoundStrategy' => Service\ConsoleRouteNotFoundStrategyFactory::class, + 'ConsoleRouter' => Router\ConsoleRouterFactory::class, + 'ConsoleViewManager' => Service\ConsoleViewManagerFactory::class, + View\DefaultRenderingStrategy::class => Service\DefaultRenderingStrategyFactory::class, + View\Renderer::class => InvokableFactory::class, + ], + ]; + } + + /** + * Provide controller plugin configuration for this component. + * + * @return array + */ + public function getPluginConfig() + { + // @codingStandardsIgnoreStart + return [ + 'aliases' => [ + 'CreateConsoleNotFoundModel' => Controller\Plugin\CreateConsoleNotFoundModel::class, + 'createConsoleNotFoundModel' => Controller\Plugin\CreateConsoleNotFoundModel::class, + 'createconsolenotfoundmodel' => Controller\Plugin\CreateConsoleNotFoundModel::class, + 'Laminas\Mvc\Controller\Plugin\CreateConsoleNotFoundModel::class' => Controller\Plugin\CreateConsoleNotFoundModel::class, + + // Legacy Zend Framework aliases + 'Zend\Mvc\Controller\Plugin\CreateConsoleNotFoundModel::class' => 'Laminas\Mvc\Controller\Plugin\CreateConsoleNotFoundModel::class', + \Zend\Mvc\Console\Controller\Plugin\CreateConsoleNotFoundModel::class => Controller\Plugin\CreateConsoleNotFoundModel::class, + ], + 'factories' => [ + Controller\Plugin\CreateConsoleNotFoundModel::class => InvokableFactory::class, + ], + ]; + // @codingStandardsIgnoreEnd + } +} diff --git a/src/Controller/AbstractConsoleController.php b/src/Controller/AbstractConsoleController.php new file mode 100644 index 0000000000000000000000000000000000000000..4061cb0bf7872a559b247894e609c2b7e23a5b26 --- /dev/null +++ b/src/Controller/AbstractConsoleController.php @@ -0,0 +1,74 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Controller; + +use Unicaen\Console\Adapter\AdapterInterface as ConsoleAdapter; +use Unicaen\Console\Request as ConsoleRequest; +use Unicaen\Console\Exception\InvalidArgumentException; +use Unicaen\Console\View\ViewModel; +use Unicaen\Mvc\Controller\AbstractActionController; +use Laminas\Stdlib\RequestInterface; +use Laminas\Stdlib\ResponseInterface; + +/** + * @method \Unicaen\Console\View\ViewModel createConsoleNotFoundModel() + */ +abstract class AbstractConsoleController extends AbstractActionController +{ + /** + * @var ConsoleAdapter + */ + protected $console; + + /** + * @param ConsoleAdapter $console + */ + public function setConsole(ConsoleAdapter $console) + { + $this->console = $console; + return $this; + } + + /** + * @return ConsoleAdapter + */ + public function getConsole() + { + return $this->console; + } + + /** + * {@inheritdoc} + */ + public function dispatch(RequestInterface $request, ResponseInterface $response = null) + { + if (! $request instanceof ConsoleRequest) { + throw new InvalidArgumentException(sprintf( + '%s can only dispatch requests in a console environment', + get_called_class() + )); + } + return parent::dispatch($request, $response); + } + + /** + * Action called if matched action does not exist. + * + * @return ViewModel + */ + public function notFoundAction() + { + $event = $this->getEvent(); + $routeMatch = $event->getRouteMatch(); + $routeMatch->setParam('action', 'not-found'); + + $helper = $this->plugin('createConsoleNotFoundModel'); + return $helper(); + } +} diff --git a/src/Controller/Plugin/CreateConsoleNotFoundModel.php b/src/Controller/Plugin/CreateConsoleNotFoundModel.php new file mode 100644 index 0000000000000000000000000000000000000000..23d960ddd7bd8ef07bce7efcfd259ddb9a5941e5 --- /dev/null +++ b/src/Controller/Plugin/CreateConsoleNotFoundModel.php @@ -0,0 +1,30 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Controller\Plugin; + +use Unicaen\Console\View\ViewModel as ConsoleModel; +use Laminas\Mvc\Controller\Plugin\AbstractPlugin; + +class CreateConsoleNotFoundModel extends AbstractPlugin +{ + /** + * Create a console view model representing a "not found" action + * + * @return ConsoleModel + */ + public function __invoke() + { + $viewModel = new ConsoleModel(); + + $viewModel->setErrorLevel(1); + $viewModel->setResult('Page not found'); + + return $viewModel; + } +} diff --git a/src/Module.php b/src/Module.php new file mode 100644 index 0000000000000000000000000000000000000000..f9a14ba30767ed7b94a08a954c774b98e5ded315 --- /dev/null +++ b/src/Module.php @@ -0,0 +1,27 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console; + +class Module +{ + /** + * Provide default configuration. + * + * @param return array + */ + public function getConfig() + { + $provider = new ConfigProvider(); + return [ + 'controller_plugins' => $provider->getPluginConfig(), + 'service_manager' => $provider->getDependencyConfig(), + 'console' => ['router' => ['routes' => []]], + ]; + } +} diff --git a/src/ResponseSender/ConsoleResponseSender.php b/src/ResponseSender/ConsoleResponseSender.php new file mode 100644 index 0000000000000000000000000000000000000000..7421bcae4c9457f1cd8744db580c97a0e88577f7 --- /dev/null +++ b/src/ResponseSender/ConsoleResponseSender.php @@ -0,0 +1,51 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\ResponseSender; + +use Laminas\Console\Response; +use Laminas\Mvc\ResponseSender\ResponseSenderInterface; +use Laminas\Mvc\ResponseSender\SendResponseEvent; + +class ConsoleResponseSender implements ResponseSenderInterface +{ + /** + * Send content + * + * @param SendResponseEvent $event + * @return ConsoleResponseSender + */ + public function sendContent(SendResponseEvent $event) + { + if ($event->contentSent()) { + return $this; + } + $response = $event->getResponse(); + echo $response->getContent(); + $event->setContentSent(); + return $this; + } + + /** + * Send the response + * + * @param SendResponseEvent $event + */ + public function __invoke(SendResponseEvent $event) + { + $response = $event->getResponse(); + if (! $response instanceof Response) { + return; + } + + $this->sendContent($event); + $errorLevel = (int) $response->getMetadata('errorLevel', 0); + $event->stopPropagation(true); + exit($errorLevel); + } +} diff --git a/src/Router/Catchall.php b/src/Router/Catchall.php new file mode 100644 index 0000000000000000000000000000000000000000..7bc91764d15d1a82e44d12404e1c264b9c40a7d8 --- /dev/null +++ b/src/Router/Catchall.php @@ -0,0 +1,107 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Laminas\Console\Request as ConsoleRequest; +use Laminas\Stdlib\RequestInterface as Request; +use Traversable; + +class Catchall implements RouteInterface +{ + /** + * Parts of the route. + * + * @var array + */ + protected $parts; + + /** + * Default values. + * + * @var array + */ + protected $defaults; + + /** + * Parameter name aliases. + * + * @var array + */ + protected $aliases; + + /** + * List of assembled parameters. + * + * @var array + */ + protected $assembledParams = []; + + /** + * Create a new simple console route. + * + * @param array $defaults + * @return Catchall + */ + public function __construct(array $defaults = []) + { + $this->defaults = $defaults; + } + + /** + * factory(): defined by Route interface. + * + * @see \Laminas\Mvc\Router\RouteInterface::factory() + * @param array|Traversable $options + * @return Simple + */ + public static function factory($options = []) + { + return new static(isset($options['defaults']) ? $options['defaults'] : []); + } + + /** + * match(): defined by Route interface. + * + * @see \Laminas\Mvc\Router\RouteInterface::match() + * @param Request $request + * @return RouteMatch + */ + public function match(Request $request) + { + if (! $request instanceof ConsoleRequest) { + return; + } + + return new RouteMatch($this->defaults); + } + + /** + * assemble(): Defined by Route interface. + * + * @see \Laminas\Mvc\Router\RouteInterface::assemble() + * @param array $params + * @param array $options + * @return mixed + */ + public function assemble(array $params = [], array $options = []) + { + $this->assembledParams = []; + } + + /** + * getAssembledParams(): defined by Route interface. + * + * @see RouteInterface::getAssembledParams + * @return array + */ + public function getAssembledParams() + { + return $this->assembledParams; + } +} diff --git a/src/Router/ConsoleRouterDelegatorFactory.php b/src/Router/ConsoleRouterDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..c8d940dc38033f2dd553079360d70a121e2431b0 --- /dev/null +++ b/src/Router/ConsoleRouterDelegatorFactory.php @@ -0,0 +1,69 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +/** + * Delegator factory for the Router service. + * + * If a console environment is detected, returns the ConsoleRouter service + * instead of the default router. + */ +class ConsoleRouterDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * Known router names/aliases; allows auto-selection of console router. + * + * @var string[] + */ + private $knownRouterNames = [ + 'router', + 'laminas\\router\routestackinterface', + ]; + + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return \Laminas\Mvc\Router\RouteStackInterface + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + // Console environment? + if ($name === 'ConsoleRouter' // force console router + || (in_array(strtolower($name), $this->knownRouterNames, true) + && Console::isConsole()) // auto detect console + ) { + return $container->get('ConsoleRouter'); + } + + return $callback(); + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return \Laminas\Mvc\Router\RouteStackInterface + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/Router/ConsoleRouterFactory.php b/src/Router/ConsoleRouterFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..aff36d376b5e4f5809d7e3e1940e6309c6c911ff --- /dev/null +++ b/src/Router/ConsoleRouterFactory.php @@ -0,0 +1,47 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Interop\Container\ContainerInterface; +use Laminas\Router\RouterConfigTrait; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ConsoleRouterFactory implements FactoryInterface +{ + use RouterConfigTrait; + + /** + * Create and return the console SimpleRouteStack. + * + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return SimpleRouteStack + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + $config = $container->has('config') ? $container->get('config') : []; + $config = isset($config['console']['router']) ? $config['console']['router'] : []; + return $this->createRouter(SimpleRouteStack::class, $config, $container); + } + + /** + * Create and return SimpleRouteStack instance + * + * For use with laminas-servicemanager v2; proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @return SimpleRouteStack + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, SimpleRouteStack::class); + } +} diff --git a/src/Router/RouteInterface.php b/src/Router/RouteInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c14bdb1651adf82ec128bacead41bf052c912784 --- /dev/null +++ b/src/Router/RouteInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Laminas\Router\RouteInterface as BaseRoute; + +interface RouteInterface extends BaseRoute +{ + /** + * Get a list of parameters used while assembling. + * + * @return array + */ + public function getAssembledParams(); +} diff --git a/src/Router/RouteMatch.php b/src/Router/RouteMatch.php new file mode 100644 index 0000000000000000000000000000000000000000..039debb1251c27612aff71152523823b6c5b2066 --- /dev/null +++ b/src/Router/RouteMatch.php @@ -0,0 +1,78 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Laminas\Router\RouteMatch as BaseRouteMatch; + +class RouteMatch extends BaseRouteMatch +{ + /** + * Length of the matched path. + * + * @var int + */ + protected $length; + + /** + * Create a RouteMatch with given parameters and length. + * + * @param array $params + * @param int $length + */ + public function __construct(array $params, $length = 0) + { + parent::__construct($params); + $this->length = $length; + } + + /** + * setMatchedRouteName(): defined by BaseRouteMatch. + * + * @see BaseRouteMatch::setMatchedRouteName() + * @param string $name + * @return self + */ + public function setMatchedRouteName($name) + { + if ($this->matchedRouteName === null) { + $this->matchedRouteName = $name; + return $this; + } + + $this->matchedRouteName = $name . '/' . $this->matchedRouteName; + + return $this; + } + + /** + * Merge parameters from another match. + * + * @param RouteMatch $match + * @return RouteMatch + */ + public function merge(RouteMatch $match) + { + $this->params = array_merge($this->params, $match->getParams()); + $this->length += $match->getLength(); + + $this->matchedRouteName = $match->getMatchedRouteName(); + + return $this; + } + + /** + * Get the matched path length. + * + * @return int + */ + public function getLength() + { + return $this->length; + } +} diff --git a/src/Router/Simple.php b/src/Router/Simple.php new file mode 100644 index 0000000000000000000000000000000000000000..4b882c5238e4cde2ddc5bbeb3e545ddd27dbca7c --- /dev/null +++ b/src/Router/Simple.php @@ -0,0 +1,147 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Laminas\Console\Request as ConsoleRequest; +use Laminas\Console\RouteMatcher\DefaultRouteMatcher; +use Laminas\Console\RouteMatcher\RouteMatcherInterface; +use Unicaen\Console\Exception; +use Laminas\Stdlib\ArrayUtils; +use Laminas\Stdlib\RequestInterface as Request; +use Traversable; + +class Simple implements RouteInterface +{ + /** + * List of assembled parameters. + * + * @var array + */ + protected $assembledParams = []; + + /** + * @var RouteMatcherInterface + */ + protected $matcher; + + /** + * Create a new simple console route. + * + * @param string|RouteMatcherInterface $routeOrRouteMatcher + * @param array $constraints + * @param array $defaults + * @param array $aliases + * @throws Exception\InvalidArgumentException + */ + public function __construct( + $routeOrRouteMatcher, + array $constraints = [], + array $defaults = [], + array $aliases = [] + ) { + if (is_string($routeOrRouteMatcher)) { + $this->matcher = new DefaultRouteMatcher($routeOrRouteMatcher, $constraints, $defaults, $aliases); + } elseif ($routeOrRouteMatcher instanceof RouteMatcherInterface) { + $this->matcher = $routeOrRouteMatcher; + } else { + throw new Exception\InvalidArgumentException( + "routeOrRouteMatcher should either be string, or class implementing RouteMatcherInterface. " + . gettype($routeOrRouteMatcher) . " was given." + ); + } + } + + /** + * factory(): defined by Route interface. + * + * @see \Laminas\Router\RouteInterface::factory() + * @param array|Traversable $options + * @throws Exception\InvalidArgumentException + * @return self + */ + public static function factory($options = []) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } elseif (! is_array($options)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable set of options', + __METHOD__ + )); + } + + if (! isset($options['route'])) { + throw new Exception\InvalidArgumentException('Missing "route" in options array'); + } + + foreach ([ + 'constraints', + 'defaults', + 'aliases', + ] as $opt) { + if (! isset($options[$opt])) { + $options[$opt] = []; + } + } + + return new static( + $options['route'], + $options['constraints'], + $options['defaults'], + $options['aliases'] + ); + } + + /** + * match(): defined by Route interface. + * + * @see \Laminas\Router\Route::match() + * @param Request $request + * @param null|int $pathOffset + * @return RouteMatch + */ + public function match(Request $request, $pathOffset = null) + { + if (! $request instanceof ConsoleRequest) { + return; + } + + $params = $request->getParams()->toArray(); + $matches = $this->matcher->match($params); + + if (null !== $matches) { + return new RouteMatch($matches); + } + return; + } + + /** + * assemble(): Defined by Route interface. + * + * @see \Laminas\Router\RouteInterface::assemble() + * @param array $params + * @param array $options + * @return mixed + */ + public function assemble(array $params = [], array $options = []) + { + $this->assembledParams = []; + } + + /** + * getAssembledParams(): defined by Route interface. + * + * @see RouteInterface::getAssembledParams + * @return array + */ + public function getAssembledParams() + { + return $this->assembledParams; + } +} diff --git a/src/Router/SimpleRouteStack.php b/src/Router/SimpleRouteStack.php new file mode 100644 index 0000000000000000000000000000000000000000..daad854114a3dda598a8b3ea3f9682448233604d --- /dev/null +++ b/src/Router/SimpleRouteStack.php @@ -0,0 +1,98 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Router; + +use Unicaen\Console\Exception; +use Laminas\Router\RouteInvokableFactory; +use Laminas\Router\SimpleRouteStack as BaseSimpleRouteStack; +use Laminas\ServiceManager\Config; +use Laminas\Stdlib\ArrayUtils; +use Traversable; + +class SimpleRouteStack extends BaseSimpleRouteStack +{ + /** + * init(): defined by SimpleRouteStack. + * + * @see BaseSimpleRouteStack::init() + */ + protected function init() + { + (new Config([ + 'aliases' => [ + 'catchall' => Catchall::class, + 'catchAll' => Catchall::class, + 'Catchall' => Catchall::class, + 'CatchAll' => Catchall::class, + 'simple' => Simple::class, + 'Simple' => Simple::class, + ], + 'factories' => [ + Catchall::class => RouteInvokableFactory::class, + Simple::class => RouteInvokableFactory::class, + + // v2 normalized names + 'laminasmvcrouterconsolecatchall' => RouteInvokableFactory::class, + 'laminasmvcrouterconsolesimple' => RouteInvokableFactory::class, + ], + ]))->configureServiceManager($this->routePluginManager); + } + + /** + * addRoute(): defined by RouteStackInterface interface. + * + * @see RouteStackInterface::addRoute() + * @param string $name + * @param mixed $route + * @param int $priority + * @return SimpleRouteStack + */ + public function addRoute($name, $route, $priority = null) + { + if (! $route instanceof RouteInterface) { + $route = $this->routeFromArray($route); + } + + return parent::addRoute($name, $route, $priority); + } + + /** + * routeFromArray(): defined by SimpleRouteStack. + * + * @see BaseSimpleRouteStack::routeFromArray() + * @param array|Traversable $specs + * @return RouteInterface + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + protected function routeFromArray($specs) + { + if ($specs instanceof Traversable) { + $specs = ArrayUtils::iteratorToArray($specs); + } + + if (! is_array($specs)) { + throw new Exception\InvalidArgumentException('Route definition must be an array or Traversable object'); + } + + // default to 'simple' console route + if (! isset($specs['type'])) { + $specs['type'] = Simple::class; + } + + // build route object + $route = parent::routeFromArray($specs); + + if (! $route instanceof RouteInterface) { + throw new Exception\RuntimeException('Given route does not implement Console route interface'); + } + + return $route; + } +} diff --git a/src/Service/ConsoleAdapterFactory.php b/src/Service/ConsoleAdapterFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..2c3a70bd35ce0401e03ffc9ee92eba681482b4f8 --- /dev/null +++ b/src/Service/ConsoleAdapterFactory.php @@ -0,0 +1,90 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Adapter\AdapterInterface; +use Laminas\Console\Console; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; +use stdClass; + +class ConsoleAdapterFactory implements FactoryInterface +{ + /** + * Create and return a Console adapter instance. + * In case we're not in a Console environment, return a dummy stdClass object. + * + * In order to disable adapter auto-detection and use a specific adapter (and charset), + * add the following fields to application configuration, for example: + * + * 'console' => array( + * 'adapter' => 'MyConsoleAdapter', // always use this console adapter + * 'charset' => 'MyConsoleCharset', // always use this console charset + * ), + * 'service_manager' => array( + * 'invokables' => array( + * 'MyConsoleAdapter' => 'Laminas\Console\Adapter\Windows', + * 'MyConsoleCharset' => 'Laminas\Console\Charset\DESCG', + * ) + * ) + * + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return AdapterInterface|stdClass + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + // First, check if we're actually in a Console environment + if (! Console::isConsole()) { + // SM factory cannot currently return null, so we return dummy object + return new stdClass(); + } + + // Read app config and determine Console adapter to use + $config = $container->get('config'); + if (! empty($config['console']) && ! empty($config['console']['adapter'])) { + // use the adapter supplied in application config + $adapter = $container->get($config['console']['adapter']); + } else { + // try to detect best console adapter + $adapter = Console::detectBestAdapter(); + $adapter = new $adapter(); + } + + // check if we have a valid console adapter + if (! $adapter instanceof AdapterInterface) { + // SM factory cannot currently return null, so we convert it to dummy object + return new stdClass(); + } + + // Optionally, change Console charset + if (! empty($config['console']) && ! empty($config['console']['charset'])) { + // use the charset supplied in application config + $charset = $container->get($config['console']['charset']); + $adapter->setCharset($charset); + } + + return $adapter; + } + + /** + * Create and return AdapterInterface instance + * + * For use with laminas-servicemanager v2; proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @return AdapterInterface|stdClass + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, AdapterInterface::class); + } +} diff --git a/src/Service/ConsoleApplicationDelegatorFactory.php b/src/Service/ConsoleApplicationDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..140e1e06f8478e04159abdb8686bff6d48df23bd --- /dev/null +++ b/src/Service/ConsoleApplicationDelegatorFactory.php @@ -0,0 +1,60 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +/** + * Delegator factory for the Application instance. + * + * If in a console environment, attaches the console view manager as an event + * listener on the Application prior to returning it. + * + * @deprecated since 1.1.8 Use the ViewManagerDelegatorFactory instead. + */ +class ConsoleApplicationDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return \Laminas\Mvc\ApplicationInterface + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + $application = $callback(); + + if (! Console::isConsole()) { + return $application; + } + + $container->get('ConsoleViewManager')->attach($application->getEventManager()); + return $application; + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return \Laminas\Mvc\ApplicationInterface + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/Service/ConsoleExceptionStrategyFactory.php b/src/Service/ConsoleExceptionStrategyFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..5e88a3919ab30b52b0ac5293712aff2397c29e55 --- /dev/null +++ b/src/Service/ConsoleExceptionStrategyFactory.php @@ -0,0 +1,74 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Unicaen\Console\View\ExceptionStrategy; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ConsoleExceptionStrategyFactory implements FactoryInterface +{ + use ConsoleViewManagerConfigTrait; + + /** + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return ExceptionStrategy + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + $strategy = new ExceptionStrategy(); + $config = $this->getConfig($container); + + $this->injectDisplayExceptions($strategy, $config); + $this->injectExceptionMessage($strategy, $config); + + return $strategy; + } + + /** + * Create and return ExceptionStrategy instance + * + * For use with laminas-servicemanager v2; proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @return ExceptionStrategy + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, ExceptionStrategy::class); + } + + /** + * Inject strategy with configured display_exceptions flag. + * + * @param ExceptionStrategy $strategy + * @param array $config + */ + private function injectDisplayExceptions(ExceptionStrategy $strategy, array $config) + { + $flag = array_key_exists('display_exceptions', $config) ? $config['display_exceptions'] : true; + $strategy->setDisplayExceptions($flag); + } + + /** + * Inject strategy with configured exception_message + * + * @param ExceptionStrategy $strategy + * @param array $config + */ + private function injectExceptionMessage(ExceptionStrategy $strategy, array $config) + { + if (isset($config['exception_message'])) { + $strategy->setMessage($config['exception_message']); + } + } +} diff --git a/src/Service/ConsoleRequestDelegatorFactory.php b/src/Service/ConsoleRequestDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..62cf5cac18869edb070041431f53104e51fe0650 --- /dev/null +++ b/src/Service/ConsoleRequestDelegatorFactory.php @@ -0,0 +1,56 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\Console\Request as ConsoleRequest; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +/** + * Delegator factory for the Request service. + * + * If a console environment is detected, returns a ConsoleRequest instead + * of the default request. + */ +class ConsoleRequestDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return ConsoleRequest|\Laminas\Http\Request + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + if (! Console::isConsole()) { + return $callback(); + } + + return new ConsoleRequest(); + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return ConsoleRequest|\Laminas\Http\Request + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/Service/ConsoleResponseDelegatorFactory.php b/src/Service/ConsoleResponseDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..1b6361c26980dafc6384dac45559c029c295135d --- /dev/null +++ b/src/Service/ConsoleResponseDelegatorFactory.php @@ -0,0 +1,56 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\Console\Response as ConsoleResponse; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +/** + * Delegator factory for the Response service. + * + * If a console environment is detected, returns a ConsoleResponse instead + * of the default response. + */ +class ConsoleResponseDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return ConsoleResponse|\Laminas\Http\Response + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + if (! Console::isConsole()) { + return $callback(); + } + + return new ConsoleResponse(); + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return ConsoleResponse|\Laminas\Http\Response + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/Service/ConsoleResponseSenderDelegatorFactory.php b/src/Service/ConsoleResponseSenderDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..67e1fd9ea0eeab77bd88730c64939a501ae0b3ac --- /dev/null +++ b/src/Service/ConsoleResponseSenderDelegatorFactory.php @@ -0,0 +1,55 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Unicaen\Console\ResponseSender\ConsoleResponseSender; +use Laminas\Mvc\ResponseSender\SendResponseEvent; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +/** + * Delegator factory for the SendResponseListener. + * + * Injects the ConsoleResponseSender as an event listener on the SendResponseListener + * prior to returning it. + */ +class ConsoleResponseSenderDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return \Laminas\Mvc\SendResponseListener + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + $sendResponseListener = $callback(); + $events = $sendResponseListener->getEventManager(); + $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new ConsoleResponseSender(), -2000); + return $sendResponseListener; + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return \Laminas\Mvc\SendResponseListener + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/Service/ConsoleRouteNotFoundStrategyFactory.php b/src/Service/ConsoleRouteNotFoundStrategyFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..080dc4900e12b27c965070c6450c7abb0ad90e63 --- /dev/null +++ b/src/Service/ConsoleRouteNotFoundStrategyFactory.php @@ -0,0 +1,58 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Unicaen\Console\View\RouteNotFoundStrategy; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ConsoleRouteNotFoundStrategyFactory implements FactoryInterface +{ + use ConsoleViewManagerConfigTrait; + + /** + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return RouteNotFoundStrategy + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + $strategy = new RouteNotFoundStrategy(); + $config = $this->getConfig($container); + + $this->injectDisplayNotFoundReason($strategy, $config); + + return $strategy; + } + + /** + * Create and return RouteNotFoundStrategy instance + * + * @param ServiceLocatorInterface $container + * @return RouteNotFoundStrategy + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, RouteNotFoundStrategy::class); + } + + /** + * Inject strategy with configured display_not_found_reason flag. + * + * @param RouteNotFoundStrategy $strategy + * @param array $config + */ + private function injectDisplayNotFoundReason(RouteNotFoundStrategy $strategy, array $config) + { + $flag = array_key_exists('display_not_found_reason', $config) ? $config['display_not_found_reason'] : true; + $strategy->setDisplayNotFoundReason($flag); + } +} diff --git a/src/Service/ConsoleViewHelperManagerDelegatorFactory.php b/src/Service/ConsoleViewHelperManagerDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..5ec124faaf7b5180f103782cc6dbb4f6e5eb5ddd --- /dev/null +++ b/src/Service/ConsoleViewHelperManagerDelegatorFactory.php @@ -0,0 +1,141 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\Router\RouteMatch; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; +use Laminas\View\Helper as ViewHelper; +use Laminas\View\HelperPluginManager; + +/** + * Delegator factory for the laminas-view helper manager. + * + * Injects the alternate Url and BasePath helper factories if the current + * environment is a console environment. + */ +class ConsoleViewHelperManagerDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return HelperPluginManager + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + $plugins = $callback(); + + if (! Console::isConsole()) { + return $plugins; + } + + return $this->injectOverrideFactories($plugins, $container); + } + + /** + * laminas-servicemanager v2 compatibility. + * + * Proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return HelperPluginManager + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } + + /** + * @param HelperPluginManager $plugins + * @param ContainerInterface $container + * @return HelperPluginManager + */ + private function injectOverrideFactories(HelperPluginManager $plugins, ContainerInterface $container) + { + $urlFactory = $this->createUrlHelperFactory($container); + $plugins->setFactory(ViewHelper\Url::class, $urlFactory); + $plugins->setFactory('laminasviewhelperurl', $urlFactory); + + $basePathFactory = $this->createBasePathHelperFactory($container); + $plugins->setFactory(ViewHelper\BasePath::class, $basePathFactory); + $plugins->setFactory('laminasviewhelperbasepath', $basePathFactory); + + return $plugins; + } + + /** + * Create and return a factory for creating a URL helper. + * + * Retrieves the application and router from the servicemanager, + * and the route match from the MvcEvent composed by the application, + * using them to configure the helper. + * + * @param ContainerInterface $services + * @return callable + */ + private function createUrlHelperFactory(ContainerInterface $services) + { + return function () use ($services) { + $helper = new ViewHelper\Url; + $helper->setRouter($services->get('HttpRouter')); + + $match = $services->get('Application') + ->getMvcEvent() + ->getRouteMatch() + ; + + if ($match instanceof RouteMatch) { + $helper->setRouteMatch($match); + } + + return $helper; + }; + } + + /** + * Create and return a factory for creating a BasePath helper. + * + * Uses configuration and request services to configure the helper. + * + * @param ContainerInterface $services + * @return callable + */ + private function createBasePathHelperFactory(ContainerInterface $services) + { + return function () use ($services) { + $config = $services->has('config') ? $services->get('config') : []; + $helper = new ViewHelper\BasePath; + + if (isset($config['view_manager']['base_path_console'])) { + $helper->setBasePath($config['view_manager']['base_path_console']); + return $helper; + } + + if (isset($config['view_manager']) && isset($config['view_manager']['base_path'])) { + $helper->setBasePath($config['view_manager']['base_path']); + return $helper; + } + + $request = $services->get('Request'); + + if (is_callable([$request, 'getBasePath'])) { + $helper->setBasePath($request->getBasePath()); + } + + return $helper; + }; + } +} diff --git a/src/Service/ConsoleViewManagerConfigTrait.php b/src/Service/ConsoleViewManagerConfigTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..a6cfb9764b3618860b0e6a8d3f9061be18e4436a --- /dev/null +++ b/src/Service/ConsoleViewManagerConfigTrait.php @@ -0,0 +1,38 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use ArrayAccess; +use Interop\Container\ContainerInterface; + +trait ConsoleViewManagerConfigTrait +{ + /** + * Retrieve view_manager configuration, if present. + * + * @param ContainerInterface $container + * @return array + */ + private function getConfig(ContainerInterface $container) + { + $config = $container->has('config') ? $container->get('config') : []; + + if (isset($config['console']['view_manager'])) { + $config = $config['console']['view_manager']; + } elseif (isset($config['view_manager'])) { + $config = $config['view_manager']; + } else { + $config = []; + } + + return (is_array($config) || $config instanceof ArrayAccess) + ? $config + : []; + } +} diff --git a/src/Service/ConsoleViewManagerFactory.php b/src/Service/ConsoleViewManagerFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..eb2d042010d9dc8295cb6693172219ec8fbcd34e --- /dev/null +++ b/src/Service/ConsoleViewManagerFactory.php @@ -0,0 +1,51 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Unicaen\Console\View\ViewManager as ConsoleViewManager; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ConsoleViewManagerFactory implements FactoryInterface +{ + /** + * Create and return the view manager for the console environment + * + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return ConsoleViewManager + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + if (! Console::isConsole()) { + throw new ServiceNotCreatedException( + 'ConsoleViewManager requires a Console environment; console environment not detected' + ); + } + + return new ConsoleViewManager(); + } + + /** + * Create and return ConsoleViewManager instance + * + * For use with laminas-servicemanager v2; proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @return ConsoleViewManager + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, ConsoleViewManager::class); + } +} diff --git a/src/Service/ControllerManagerDelegatorFactory.php b/src/Service/ControllerManagerDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..f37c6803e954ee456537f46e6f28f7732681cb80 --- /dev/null +++ b/src/Service/ControllerManagerDelegatorFactory.php @@ -0,0 +1,75 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Unicaen\Console\Controller\AbstractConsoleController; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ControllerManagerDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * Add a ControllerManager initializer to inject the console into AbstractConsoleController instances. + * + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return \Laminas\Mvc\Controller\ControllerManager + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + $controllers = $callback(); + $controllers->addInitializer([$this, 'injectConsole']); + return $controllers; + } + + /** + * Add a ControllerManager initializer to inject the console into AbstractConsoleController instances. (v2) + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return \Laminas\Mvc\Controller\ControllerManager + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } + + /** + * Initializer: inject a Console instance into AbstractConsoleController instances. + * + * @param ContainerInterface|mixed $first ContainerInterface under + * laminas-servicemanager v3, instance to inspect under v2. + * @param mixed|ServiceLocatorInterface $second Instance to inspect + * under laminas-servicemanager v3, plugin manager under v3. + * @return void + */ + public function injectConsole($first, $second) + { + if ($first instanceof ContainerInterface) { + // v3 + $container = $first; + $controller = $second; + } else { + // For v2, we need to pull the parent service locator + $container = $second->getServiceLocator() ?: $second; + $controller = $first; + } + + if (! $controller instanceof AbstractConsoleController) { + return; + } + + $controller->setConsole($container->get('Console')); + } +} diff --git a/src/Service/DefaultRenderingStrategyFactory.php b/src/Service/DefaultRenderingStrategyFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..77b1926cc6b9d1f8266a6c9ba20b75db6ad92539 --- /dev/null +++ b/src/Service/DefaultRenderingStrategyFactory.php @@ -0,0 +1,45 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Unicaen\Console\View\DefaultRenderingStrategy; +use Unicaen\Console\View\Renderer; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class DefaultRenderingStrategyFactory implements FactoryInterface +{ + /** + * Create and return DefaultRenderingStrategy (v3) + * + * @param ContainerInterface $container + * @param string $name + * @param null|array $options + * @return DefaultRenderingStrategy + */ + public function __invoke(ContainerInterface $container, $name, array $options = null) + { + return new DefaultRenderingStrategy($container->get(Renderer::class)); + } + + /** + * Create and return DefaultRenderingStrategy (v2) + * + * @param ServiceLocatorInterface $container + * @param null|string $name + * @param null|string $requestedName + * @return DefaultRenderingStrategy + */ + public function createService(ServiceLocatorInterface $container, $name = null, $requestedName = null) + { + $requestedName = $requestedName ?: Renderer::class; + return $this($container, $requestedName); + } +} diff --git a/src/Service/ViewManagerDelegatorFactory.php b/src/Service/ViewManagerDelegatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..04c96ab527f392ad6c413a35ff93bf9d62848344 --- /dev/null +++ b/src/Service/ViewManagerDelegatorFactory.php @@ -0,0 +1,49 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\Service; + +use Interop\Container\ContainerInterface; +use Laminas\Console\Console; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ViewManagerDelegatorFactory implements DelegatorFactoryInterface +{ + /** + * Return a ConsoleViewManager if in a Console environment. + * + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return \Unicaen\Console\View\ViewManager|Laminas\Mvc\View\Http\ViewManager + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + if (! Console::isConsole() || ! $container->has('ConsoleViewManager')) { + return $callback(); + } + + return $container->get('ConsoleViewManager'); + } + + /** + * Return a ConsoleViewManager if in a Console environment. (v2) + * + * @param ServiceLocatorInterface $container + * @param string $name + * @param string $requestedName + * @param callable $callback + * @return \Unicaen\Console\View\ViewManager|Laminas\Mvc\View\Http\ViewManager + */ + public function createDelegatorWithName(ServiceLocatorInterface $container, $name, $requestedName, $callback) + { + return $this($container, $requestedName, $callback); + } +} diff --git a/src/View/CreateViewModelListener.php b/src/View/CreateViewModelListener.php new file mode 100644 index 0000000000000000000000000000000000000000..7a08029a564712da380b0a2474fe26d5cb505881 --- /dev/null +++ b/src/View/CreateViewModelListener.php @@ -0,0 +1,83 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\EventManager\AbstractListenerAggregate; +use Laminas\EventManager\EventManagerInterface as Events; +use Unicaen\Console\View\ViewModel as ConsoleModel; +use Laminas\Mvc\MvcEvent; +use Laminas\Stdlib\ArrayUtils; + +class CreateViewModelListener extends AbstractListenerAggregate +{ + /** + * {@inheritDoc} + */ + public function attach(Events $events, $priority = 1) + { + $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'createViewModelFromString'], -80); + $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'createViewModelFromArray'], -80); + $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'createViewModelFromNull'], -80); + } + + /** + * Inspect the result, and cast it to a ViewModel if a string is detected + * + * @param MvcEvent $e + * @return void + */ + public function createViewModelFromString(MvcEvent $e) + { + $result = $e->getResult(); + if (! is_string($result)) { + return; + } + + // create Console model + $model = new ConsoleModel; + + // store the result in a model variable + $model->setVariable(ConsoleModel::RESULT, $result); + $e->setResult($model); + } + + /** + * Inspect the result, and cast it to a ViewModel if an assoc array is detected + * + * @param MvcEvent $e + * @return void + */ + public function createViewModelFromArray(MvcEvent $e) + { + $result = $e->getResult(); + if (! ArrayUtils::hasStringKeys($result, true)) { + return; + } + + $model = new ConsoleModel($result); + $e->setResult($model); + } + + /** + * Inspect the result, and cast it to a ViewModel if null is detected + * + * @param MvcEvent $e + * @return void + */ + public function createViewModelFromNull(MvcEvent $e) + { + $result = $e->getResult(); + if (null !== $result) { + return; + } + + $model = new ConsoleModel; + $e->setResult($model); + } +} diff --git a/src/View/DefaultRenderingStrategy.php b/src/View/DefaultRenderingStrategy.php new file mode 100644 index 0000000000000000000000000000000000000000..1c4d8a1d222d4117a473259c54e13127414ff509 --- /dev/null +++ b/src/View/DefaultRenderingStrategy.php @@ -0,0 +1,83 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\Console\Response as ConsoleResponse; +use Laminas\EventManager\AbstractListenerAggregate; +use Laminas\EventManager\EventManagerInterface; +use Unicaen\Console\View\ViewModel as ConsoleViewModel; +use Laminas\Mvc\MvcEvent; +use Laminas\Stdlib\ResponseInterface as Response; + +class DefaultRenderingStrategy extends AbstractListenerAggregate +{ + /** + * @var Renderer + */ + private $renderer; + + /** + * @param Renderer $renderer + */ + public function __construct(Renderer $renderer) + { + $this->renderer = $renderer; + } + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, [$this, 'render'], -10000); + } + + /** + * Render the view + * + * @param MvcEvent $e + * @return Response + */ + public function render(MvcEvent $e) + { + $result = $e->getResult(); + if ($result instanceof Response) { + return $result; // the result is already rendered ... + } + + // Marshal arguments + $response = $e->getResponse(); + + // Render the result + $responseText = $this->renderer->render($result); + + // Fetch service manager + $sm = $e->getApplication()->getServiceManager(); + + // Fetch console + $console = $sm->get('console'); + + // Append console response to response object + $content = $response->getContent() . $responseText; + if (is_callable([$console, 'encodeText'])) { + $content = $console->encodeText($content); + } + $response->setContent($content); + + // Pass on console-specific options + if ($response instanceof ConsoleResponse + && $result instanceof ConsoleViewModel + ) { + $errorLevel = $result->getErrorLevel(); + $response->setErrorLevel($errorLevel); + } + + return $response; + } +} diff --git a/src/View/ExceptionStrategy.php b/src/View/ExceptionStrategy.php new file mode 100644 index 0000000000000000000000000000000000000000..a516a4555379f4050e9a90502f0444746d4f798e --- /dev/null +++ b/src/View/ExceptionStrategy.php @@ -0,0 +1,271 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\EventManager\AbstractListenerAggregate; +use Laminas\EventManager\EventManagerInterface; +use Laminas\Mvc\Application; +use Unicaen\Console\View\ViewModel as ConsoleModel; +use Laminas\Mvc\MvcEvent; +use Laminas\Stdlib\ResponseInterface as Response; + +class ExceptionStrategy extends AbstractListenerAggregate +{ + /** + * Display exceptions? + * @var bool + */ + protected $displayExceptions = true; + + /** + * A template for message to show in console when an exception has occurred. + * @var string|callable + */ + protected $message = <<<EOT +====================================================================== + The application has thrown an exception! +====================================================================== + :className + :message +---------------------------------------------------------------------- +:file::line +:stack +====================================================================== + Previous Exception(s): +:previous + +EOT; + + /** + * A template for message to show in console when an exception has previous exceptions. + * @var string + */ + protected $previousMessage = <<<EOT +====================================================================== + :className + :message +---------------------------------------------------------------------- +:file::line +:stack + +EOT; + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'prepareExceptionViewModel']); + $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$this, 'prepareExceptionViewModel']); + } + + /** + * Flag: display exceptions in error pages? + * + * @param bool $displayExceptions + * @return ExceptionStrategy + */ + public function setDisplayExceptions($displayExceptions) + { + $this->displayExceptions = (bool) $displayExceptions; + return $this; + } + + /** + * Should we display exceptions in error pages? + * + * @return bool + */ + public function displayExceptions() + { + return $this->displayExceptions; + } + + /** + * Get current template for message that will be shown in Console. + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Set template for message that will be shown in Console. + * The message can be a string (template) or a callable (i.e. a closure). + * + * The closure is expected to return a string and will be called with 2 parameters: + * Exception $exception - the exception being thrown + * boolean $displayExceptions - whether to display exceptions or not + * + * If the message is a string, one can use the following template params: + * + * :className - full class name of exception instance + * :message - exception message + * :code - exception code + * :file - the file where the exception has been thrown + * :line - the line where the exception has been thrown + * :stack - full exception stack + * + * @param string|callable $message + * @return ExceptionStrategy + */ + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + /** + * Sets template for previous message that will be shown in Console. + * + * @param string $previousMessage + * @return ExceptionStrategy + */ + public function setPreviousMessage($previousMessage) + { + $this->previousMessage = $previousMessage; + return $this; + } + + /** + * @return callable|string + */ + public function getPreviousMessage() + { + return $this->previousMessage; + } + + /** + * Create an exception view model, and set the HTTP status code + * + * @todo dispatch.error does not halt dispatch unless a response is + * returned. As such, we likely need to trigger rendering as a low + * priority dispatch.error event (or goto a render event) to ensure + * rendering occurs, and that munging of view models occurs when + * expected. + * @param MvcEvent $e + * @return void + */ + public function prepareExceptionViewModel(MvcEvent $e) + { + // Do nothing if no error in the event + $error = $e->getError(); + if (empty($error)) { + return; + } + + // Do nothing if the result is a response object + $result = $e->getResult(); + if ($result instanceof Response) { + return; + } + + switch ($error) { + case Application::ERROR_CONTROLLER_NOT_FOUND: + case Application::ERROR_CONTROLLER_INVALID: + case Application::ERROR_ROUTER_NO_MATCH: + // Specifically not handling these because they are handled by routeNotFound strategy + return; + + case Application::ERROR_EXCEPTION: + default: + // Prepare error message + $exception = $e->getParam('exception'); + + if (is_callable($this->message)) { + $callback = $this->message; + $message = (string) $callback($exception, $this->displayExceptions); + } elseif ($this->displayExceptions + // @todo clean up once PHP 7 requirement is enforced + && ($exception instanceof \Exception || $exception instanceof \Throwable) + ) { + $previous = ''; + $previousException = $exception->getPrevious(); + while ($previousException) { + $previous .= str_replace( + [ + ':className', + ':message', + ':code', + ':file', + ':line', + ':stack', + ], + [ + get_class($previousException), + $previousException->getMessage(), + $previousException->getCode(), + $previousException->getFile(), + $previousException->getLine(), + $exception->getTraceAsString(), + ], + $this->previousMessage + ); + $previousException = $previousException->getPrevious(); + } + + $message = str_replace( + [ + ':className', + ':message', + ':code', + ':file', + ':line', + ':stack', + ':previous', + ], + [ + get_class($exception), + $exception->getMessage(), + $exception->getCode(), + $exception->getFile(), + $exception->getLine(), + $exception->getTraceAsString(), + $previous + ], + $this->message + ); + } else { + $message = str_replace( + [ + ':className', + ':message', + ':code', + ':file', + ':line', + ':stack', + ':previous', + ], + [ + '', + '', + '', + '', + '', + '', + '', + ], + $this->message + ); + } + + // Prepare view model + $model = new ConsoleModel(); + $model->setResult($message); + $model->setErrorLevel(1); + + // Inject it into MvcEvent + $e->setResult($model); + + break; + } + } +} diff --git a/src/View/InjectNamedConsoleParamsListener.php b/src/View/InjectNamedConsoleParamsListener.php new file mode 100644 index 0000000000000000000000000000000000000000..b7a83bc3c049286b0fa5ffa8a8eaa6afbdcac8c0 --- /dev/null +++ b/src/View/InjectNamedConsoleParamsListener.php @@ -0,0 +1,50 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\Console\Request as ConsoleRequest; +use Laminas\EventManager\AbstractListenerAggregate; +use Laminas\EventManager\EventManagerInterface as Events; +use Laminas\Mvc\MvcEvent; + +class InjectNamedConsoleParamsListener extends AbstractListenerAggregate +{ + /** + * {@inheritDoc} + */ + public function attach(Events $events, $priority = 1) + { + $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'injectNamedParams'], -80); + } + + /** + * Inspect the result, and cast it to a ViewModel if a string is detected + * + * @param MvcEvent $e + * @return void + */ + public function injectNamedParams(MvcEvent $e) + { + if (! $routeMatch = $e->getRouteMatch()) { + return; // cannot work without route match + } + + $request = $e->getRequest(); + if (! $request instanceof ConsoleRequest) { + return; // will not inject non-console requests + } + + // Inject route match params into request + $params = array_merge( + $request->getParams()->toArray(), + $routeMatch->getParams() + ); + $request->getParams()->fromArray($params); + } +} diff --git a/src/View/InjectViewModelListener.php b/src/View/InjectViewModelListener.php new file mode 100644 index 0000000000000000000000000000000000000000..5e66f54fbbc5e2473e07fd83873e99336e1c5fd7 --- /dev/null +++ b/src/View/InjectViewModelListener.php @@ -0,0 +1,15 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\Mvc\View\Http\InjectViewModelListener as HttpInjectViewModelListener; + +class InjectViewModelListener extends HttpInjectViewModelListener +{ +} diff --git a/src/View/Renderer.php b/src/View/Renderer.php new file mode 100644 index 0000000000000000000000000000000000000000..7e81db51eaf9ae425fd891f54d39afd1f94ab74b --- /dev/null +++ b/src/View/Renderer.php @@ -0,0 +1,140 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\Filter\FilterChain; +use Laminas\View\Model\ModelInterface; +use Laminas\View\Renderer\RendererInterface; +use Laminas\View\Renderer\TreeRendererInterface; +use Laminas\View\Resolver\ResolverInterface; + +/** + * Render console view models. + */ +class Renderer implements RendererInterface, TreeRendererInterface +{ + /** + * @var FilterChain + */ + protected $filterChain; + + /** + * Constructor. + * + * @param array $config Configuration key-value pairs. + */ + public function __construct(FilterChain $filterChain = null) + { + if ($filterChain) { + $this->setFilterChain($filterChain); + } + } + + /** + * Set the script resolver. + * + * No-op. Required by RendererInterface. + * + * @param ResolverInterface $resolver + * @return void + */ + public function setResolver(ResolverInterface $resolver) + { + } + + /** + * Return the template engine object. + * + * Returns the object instance, as it is its own template engine. + * + * @return self + */ + public function getEngine() + { + return $this; + } + + /** + * Set filter chain o use for post-filtering script content. + * + * @param FilterChain $filters + */ + public function setFilterChain(FilterChain $filters) + { + $this->filterChain = $filters; + } + + /** + * Retrieve filter chain for post-filtering script content. + * + * @return null|FilterChain + */ + public function getFilterChain() + { + return $this->filterChain; + } + + /** + * Recursively processes all ViewModels and returns output. + * + * @param string|ModelInterface $model A ViewModel instance. + * @param null|array|\Traversable $values Values to use when rendering. If + * none provided, uses those in the composed variables container. + * @return string Console output. + */ + public function render($model, $values = null) + { + $result = ''; + + if (! $model instanceof ModelInterface) { + // View model is required by this renderer + return $result; + } + + // If option keys match setters, pass values to those methods. + foreach ($model->getOptions() as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + + // Render children first + if ($model->hasChildren()) { + // recursively render all children + foreach ($model->getChildren() as $child) { + $result .= $this->render($child, $values); + } + } + + // Render the result, if present. + $values = $model->getVariables(); + + if (isset($values['result']) && ! isset($this->filterChain)) { + // append the result verbatim + $result .= $values['result']; + } + + if (isset($values['result']) && isset($this->filterChain)) { + // filter and append the result + $result .= $this->getFilterChain()->filter($values['result']); + } + + return $result; + } + + /** + * @see Laminas\View\Renderer\TreeRendererInterface + * @return bool + */ + public function canRenderTrees() + { + return true; + } +} diff --git a/src/View/RouteNotFoundStrategy.php b/src/View/RouteNotFoundStrategy.php new file mode 100644 index 0000000000000000000000000000000000000000..c2e87bd08ccd31b6bd489a22b43b82f5281baf6a --- /dev/null +++ b/src/View/RouteNotFoundStrategy.php @@ -0,0 +1,473 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\Console\Adapter\AdapterInterface as ConsoleAdapter; +use Laminas\Console\ColorInterface; +use Laminas\Console\Request as ConsoleRequest; +use Laminas\Console\Response as ConsoleResponse; +use Laminas\EventManager\AbstractListenerAggregate; +use Laminas\EventManager\EventManagerInterface; +use Laminas\ModuleManager\Feature\ConsoleBannerProviderInterface; +use Laminas\ModuleManager\Feature\ConsoleUsageProviderInterface; +use Laminas\ModuleManager\ModuleManagerInterface; +use Laminas\Mvc\Application; +use Unicaen\Console\Exception\RuntimeException; +use Unicaen\Console\View\ViewModel as ConsoleModel; +use Laminas\Mvc\MvcEvent; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\Stdlib\ResponseInterface as Response; +use Laminas\Stdlib\StringUtils; +use Laminas\Text\Table; + +class RouteNotFoundStrategy extends AbstractListenerAggregate +{ + /** + * Whether or not to display the reason for routing failure + * + * @var bool + */ + protected $displayNotFoundReason = true; + + /** + * The reason for a not-found condition + * + * @var bool|string + */ + protected $reason = false; + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->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 (! $banners) { + return "Laminas 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 (! $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)) { + $count = count($b); + if (($count !== $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; + $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 Laminas\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 \Laminas\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; + } +} diff --git a/src/View/ViewManager.php b/src/View/ViewManager.php new file mode 100644 index 0000000000000000000000000000000000000000..d6a2e1987904899d4873a0899b4fefe171e71d4b --- /dev/null +++ b/src/View/ViewManager.php @@ -0,0 +1,221 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use ArrayAccess; +use Laminas\EventManager\EventManagerInterface; +use Laminas\EventManager\ListenerAggregateInterface; +use Laminas\EventManager\ListenerAggregateTrait; +use Laminas\Mvc\MvcEvent; +use Laminas\Stdlib\DispatchableInterface; +use Laminas\View\View; +use Traversable; + +/** + * Prepares the view layer for console applications + */ +class ViewManager implements ListenerAggregateInterface +{ + use ListenerAggregateTrait; + + /** + * @var object application configuration service + */ + protected $config; + + /** + * @var MvcEvent + */ + protected $event; + + /** + * @var ServiceManager + */ + protected $services; + + /** + * @var View + */ + protected $view; + + /** + * Attach bootstrap event. + * + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], 10000); + } + + /** + * Prepares the view layer + * + * Overriding, as several operations are omitted in the console view + * algorithms, as well as to ensure we pick up the Console variants + * of several listeners and strategies. + * + * @param MvcEvent $event + * @return void + */ + public function onBootstrap($event) + { + $application = $event->getApplication(); + $services = $application->getServiceManager(); + $events = $application->getEventManager(); + $sharedEvents = $events->getSharedManager(); + $this->config = $this->loadConfig($services->get('config')); + $this->services = $services; + + $routeNotFoundStrategy = $services->get('ConsoleRouteNotFoundStrategy'); + $exceptionStrategy = $services->get('ConsoleExceptionStrategy'); + $mvcRenderingStrategy = $services->get('ConsoleDefaultRenderingStrategy'); + $createViewModelListener = new CreateViewModelListener(); + $injectViewModelListener = new InjectViewModelListener(); + $injectParamsListener = new InjectNamedConsoleParamsListener(); + + $this->registerMvcRenderingStrategies($events); + $this->registerViewStrategies(); + + // @codingStandardsIgnoreStart + $routeNotFoundStrategy->attach($events); + $exceptionStrategy->attach($events); + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$injectViewModelListener, 'injectViewModel'], -100); + $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$injectViewModelListener, 'injectViewModel'], -100); + $mvcRenderingStrategy->attach($events); + + $sharedEvents->attach(DispatchableInterface::class, MvcEvent::EVENT_DISPATCH, [$injectParamsListener, 'injectNamedParams'], 1000); + $sharedEvents->attach(DispatchableInterface::class, MvcEvent::EVENT_DISPATCH, [$createViewModelListener, 'createViewModelFromArray'], -80); + $sharedEvents->attach(DispatchableInterface::class, MvcEvent::EVENT_DISPATCH, [$createViewModelListener, 'createViewModelFromString'], -80); + $sharedEvents->attach(DispatchableInterface::class, MvcEvent::EVENT_DISPATCH, [$createViewModelListener, 'createViewModelFromNull'], -80); + $sharedEvents->attach(DispatchableInterface::class, MvcEvent::EVENT_DISPATCH, [$injectViewModelListener, 'injectViewModel'], -100); + // @codingStandardsIgnoreEnd + } + + /** + * Retrieves the View instance + * + * @return View + */ + public function getView() + { + if ($this->view) { + return $this->view; + } + + $this->view = $this->services->get(View::class); + return $this->view; + } + + /** + * Extract view manager configuration from the application's configuration + * + * @param array|ArrayAccess $configService + * @return array|ArrayAccess + */ + private function loadConfig($configService) + { + $config = []; + + // override when console config is provided, otherwise use the standard definition + if (isset($configService['console']['view_manager'])) { + $config = $configService['console']['view_manager']; + } elseif (isset($configService['view_manager'])) { + $config = $configService['view_manager']; + } + + return ($config instanceof ArrayAccess || is_array($config)) + ? $config + : []; + } + + /** + * Register additional mvc rendering strategies + * + * If there is a "mvc_strategies" key of the view manager configuration, loop + * through it. Pull each as a service from the service manager, and, if it + * is a ListenerAggregate, attach it to the view, at priority 100. This + * latter allows each to trigger before the default mvc rendering strategy, + * and for them to trigger in the order they are registered. + * + * @param EventManagerInterface $events + * @return void + */ + private function registerMvcRenderingStrategies(EventManagerInterface $events) + { + if (! isset($this->config['mvc_strategies'])) { + return; + } + + $mvcStrategies = $this->config['mvc_strategies']; + + if (is_string($mvcStrategies)) { + $mvcStrategies = [$mvcStrategies]; + } + + if (! is_array($mvcStrategies) && ! $mvcStrategies instanceof Traversable) { + return; + } + + foreach ($mvcStrategies as $mvcStrategy) { + if (! is_string($mvcStrategy)) { + continue; + } + + $listener = $this->services->get($mvcStrategy); + if (! $listener instanceof ListenerAggregateInterface) { + continue; + } + + $listener->attach($events, 100); + } + } + + /** + * Register additional view strategies + * + * If there is a "strategies" key of the view manager configuration, loop + * through it. Pull each as a service from the service manager, and, if it + * is a ListenerAggregate, attach it to the view, at priority 100. This + * latter allows each to trigger before the default strategy, and for them + * to trigger in the order they are registered. + * + * @return void + */ + private function registerViewStrategies() + { + if (! isset($this->config['strategies'])) { + return; + } + + $strategies = $this->config['strategies']; + + if (is_string($strategies)) { + $strategies = [$strategies]; + } + + if (! is_array($strategies) && ! $strategies instanceof Traversable) { + return; + } + + $view = $this->getView(); + $events = $view->getEventManager(); + + foreach ($strategies as $strategy) { + if (! is_string($strategy)) { + continue; + } + + $listener = $this->services->get($strategy); + if ($listener instanceof ListenerAggregateInterface) { + $listener->attach($events, 100); + } + } + } +} diff --git a/src/View/ViewModel.php b/src/View/ViewModel.php new file mode 100644 index 0000000000000000000000000000000000000000..661ff7d09392444d7107acee185c6072ac68cbd6 --- /dev/null +++ b/src/View/ViewModel.php @@ -0,0 +1,72 @@ +<?php + +/** + * @see https://github.com/laminas/laminas-mvc-console for the canonical source repository + * @copyright https://github.com/laminas/laminas-mvc-console/blob/master/COPYRIGHT.md + * @license https://github.com/laminas/laminas-mvc-console/blob/master/LICENSE.md New BSD License + */ + +namespace Unicaen\Console\View; + +use Laminas\View\Model\ViewModel as BaseViewModel; + +class ViewModel extends BaseViewModel +{ + const RESULT = 'result'; + + /** + * Console output does not support containers. + * + * @var string + */ + protected $captureTo = null; + + /** + * Console output should always be terminal. + * + * @var bool + */ + protected $terminate = true; + + /** + * Set error level to return after the application ends. + * + * @param int $errorLevel + */ + public function setErrorLevel($errorLevel) + { + $this->options['errorLevel'] = $errorLevel; + } + + /** + * @return int + */ + public function getErrorLevel() + { + if (array_key_exists('errorLevel', $this->options)) { + return $this->options['errorLevel']; + } + } + + /** + * Set result text. + * + * @param string $text + * @return self + */ + public function setResult($text) + { + $this->setVariable(self::RESULT, $text); + return $this; + } + + /** + * Get result text. + * + * @return mixed + */ + public function getResult() + { + return $this->getVariable(self::RESULT); + } +}