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);
+    }
+}