├── src ├── Exception │ ├── ExceptionInterface.php │ ├── MissingLocatorException.php │ ├── DomainException.php │ ├── RuntimeException.php │ ├── InvalidPluginException.php │ ├── InvalidControllerException.php │ ├── BadMethodCallException.php │ ├── InvalidArgumentException.php │ ├── ReachedFinalHandlerException.php │ └── InvalidMiddlewareException.php ├── Service │ ├── PaginatorPluginManagerFactory.php │ ├── ControllerPluginManagerFactory.php │ ├── SendResponseListenerFactory.php │ ├── RequestFactory.php │ ├── ResponseFactory.php │ ├── ViewManagerFactory.php │ ├── DispatchListenerFactory.php │ ├── HttpViewManagerFactory.php │ ├── ViewPhpRendererStrategyFactory.php │ ├── ViewPhpRendererFactory.php │ ├── ViewFactory.php │ ├── HttpViewManagerConfigTrait.php │ ├── EventManagerFactory.php │ ├── ViewFeedStrategyFactory.php │ ├── ApplicationFactory.php │ ├── InjectTemplateListenerFactory.php │ ├── AbstractPluginManagerFactory.php │ ├── ViewJsonStrategyFactory.php │ ├── HttpMethodListenerFactory.php │ ├── ConfigFactory.php │ ├── ViewPrefixPathStackResolverFactory.php │ ├── ViewTemplateMapResolverFactory.php │ ├── ControllerManagerFactory.php │ ├── HttpDefaultRenderingStrategyFactory.php │ ├── ViewTemplatePathStackFactory.php │ ├── ViewResolverFactory.php │ ├── HttpExceptionStrategyFactory.php │ ├── HttpRouteNotFoundStrategyFactory.php │ ├── ModuleManagerFactory.php │ ├── ServiceManagerConfig.php │ └── ViewHelperManagerFactory.php ├── ResponseSender │ ├── ResponseSenderInterface.php │ ├── PhpEnvironmentResponseSender.php │ ├── AbstractResponseSender.php │ ├── HttpResponseSender.php │ ├── SimpleStreamResponseSender.php │ └── SendResponseEvent.php ├── InjectApplicationEventInterface.php ├── Controller │ ├── Plugin │ │ ├── PluginInterface.php │ │ ├── CreateHttpNotFoundModel.php │ │ ├── AbstractPlugin.php │ │ ├── Service │ │ │ └── ForwardFactory.php │ │ ├── Layout.php │ │ ├── Url.php │ │ ├── Redirect.php │ │ ├── Params.php │ │ └── AcceptableViewModelSelector.php │ ├── AbstractActionController.php │ ├── ControllerManager.php │ ├── MiddlewareController.php │ ├── PluginManager.php │ ├── LazyControllerAbstractFactory.php │ └── AbstractController.php ├── ApplicationInterface.php ├── View │ └── Http │ │ ├── CreateViewModelListener.php │ │ ├── InjectViewModelListener.php │ │ ├── InjectRoutematchParamsListener.php │ │ ├── DefaultRenderingStrategy.php │ │ ├── ExceptionStrategy.php │ │ ├── InjectTemplateListener.php │ │ ├── RouteNotFoundStrategy.php │ │ └── ViewManager.php ├── RouteListener.php ├── ModuleRouteListener.php ├── HttpMethodListener.php ├── SendResponseListener.php ├── MiddlewareListener.php ├── MvcEvent.php └── DispatchListener.php ├── README.md ├── LICENSE.md └── composer.json /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | setEventManager($container->get('EventManager')); 23 | return $listener; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Controller/Plugin/PluginInterface.php: -------------------------------------------------------------------------------- 1 | setStatusCode(404); 25 | 26 | return new ViewModel(['content' => 'Page not found']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Service/RequestFactory.php: -------------------------------------------------------------------------------- 1 | get('HttpViewManager'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Service/DispatchListenerFactory.php: -------------------------------------------------------------------------------- 1 | get('ControllerManager')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Service/HttpViewManagerFactory.php: -------------------------------------------------------------------------------- 1 | get(PhpRenderer::class)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ResponseSender/PhpEnvironmentResponseSender.php: -------------------------------------------------------------------------------- 1 | getResponse(); 23 | if (! $response instanceof Response) { 24 | return $this; 25 | } 26 | 27 | $this->sendHeaders($event) 28 | ->sendContent($event); 29 | $event->stopPropagation(true); 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Service/ViewPhpRendererFactory.php: -------------------------------------------------------------------------------- 1 | setHelperPluginManager($container->get('ViewHelperManager')); 26 | $renderer->setResolver($container->get('ViewResolver')); 27 | 28 | return $renderer; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ApplicationInterface.php: -------------------------------------------------------------------------------- 1 | controller = $controller; 28 | } 29 | 30 | /** 31 | * Get the current controller instance 32 | * 33 | * @return null|Dispatchable 34 | */ 35 | public function getController() 36 | { 37 | return $this->controller; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Service/ViewFactory.php: -------------------------------------------------------------------------------- 1 | get('EventManager'); 27 | 28 | $view->setEventManager($events); 29 | $container->get(PhpRendererStrategy::class)->attach($events); 30 | 31 | return $view; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/HttpViewManagerConfigTrait.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 24 | 25 | if (isset($config['view_manager']) 26 | && (is_array($config['view_manager']) 27 | || $config['view_manager'] instanceof ArrayAccess 28 | ) 29 | ) { 30 | return $config['view_manager']; 31 | } 32 | 33 | return []; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Service/EventManagerFactory.php: -------------------------------------------------------------------------------- 1 | has('SharedEventManager') ? $container->get('SharedEventManager') : null; 30 | 31 | return new EventManager($shared); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/ViewFeedStrategyFactory.php: -------------------------------------------------------------------------------- 1 | get('ViewFeedRenderer')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/ApplicationFactory.php: -------------------------------------------------------------------------------- 1 | get('EventManager'), 32 | $container->get('Request'), 33 | $container->get('Response') 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Service/InjectTemplateListenerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 27 | 28 | if (isset($config['view_manager']['controller_map']) 29 | && (is_array($config['view_manager']['controller_map'])) 30 | ) { 31 | $listener->setControllerMap($config['view_manager']['controller_map']); 32 | } 33 | 34 | return $listener; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exception/InvalidMiddlewareException.php: -------------------------------------------------------------------------------- 1 | middlewareName = $middlewareName; 26 | return $instance; 27 | } 28 | 29 | public static function fromNull() 30 | { 31 | $instance = new self('Middleware name cannot be null'); 32 | return $instance; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function toMiddlewareName() 39 | { 40 | return null !== $this->middlewareName ? $this->middlewareName : ''; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Service/AbstractPluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | headersSent()) { 23 | return $this; 24 | } 25 | 26 | $response = $event->getResponse(); 27 | 28 | foreach ($response->getHeaders() as $header) { 29 | if ($header instanceof MultipleHeaderInterface) { 30 | header($header->toString(), false); 31 | continue; 32 | } 33 | header($header->toString()); 34 | } 35 | 36 | $status = $response->renderStatusLine(); 37 | header($status); 38 | 39 | $event->setHeadersSent(); 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Service/ViewJsonStrategyFactory.php: -------------------------------------------------------------------------------- 1 | get('ViewJsonRenderer'); 32 | $jsonStrategy = new JsonStrategy($jsonRenderer); 33 | return $jsonStrategy; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Controller/Plugin/Service/ForwardFactory.php: -------------------------------------------------------------------------------- 1 | has('ControllerManager')) { 26 | throw new ServiceNotCreatedException(sprintf( 27 | '%s requires that the application service manager contains a "%s" service; none found', 28 | __CLASS__, 29 | 'ControllerManager' 30 | )); 31 | } 32 | $controllers = $container->get('ControllerManager'); 33 | 34 | return new Forward($controllers); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Service/HttpMethodListenerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 23 | 24 | if (! isset($config['http_methods_listener'])) { 25 | return new HttpMethodListener(); 26 | } 27 | 28 | $listenerConfig = $config['http_methods_listener']; 29 | $enabled = array_key_exists('enabled', $listenerConfig) 30 | ? $listenerConfig['enabled'] 31 | : true; 32 | $allowedMethods = (isset($listenerConfig['allowed_methods']) && is_array($listenerConfig['allowed_methods'])) 33 | ? $listenerConfig['allowed_methods'] 34 | : null; 35 | 36 | return new HttpMethodListener($enabled, $allowedMethods); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Service/ConfigFactory.php: -------------------------------------------------------------------------------- 1 | get('ModuleManager'); 33 | $moduleManager->loadModules(); 34 | $moduleParams = $moduleManager->getEvent()->getParams(); 35 | return $moduleParams['configListener']->getMergedConfig(false); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/ViewPrefixPathStackResolverFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 30 | $prefixes = []; 31 | 32 | if (isset($config['view_manager']['prefix_template_path_stack'])) { 33 | $prefixes = $config['view_manager']['prefix_template_path_stack']; 34 | } 35 | 36 | return new PrefixPathStackResolver($prefixes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ResponseSender/HttpResponseSender.php: -------------------------------------------------------------------------------- 1 | contentSent()) { 23 | return $this; 24 | } 25 | $response = $event->getResponse(); 26 | echo $response->getContent(); 27 | $event->setContentSent(); 28 | return $this; 29 | } 30 | 31 | /** 32 | * Send HTTP response 33 | * 34 | * @param SendResponseEvent $event 35 | * @return HttpResponseSender 36 | */ 37 | public function __invoke(SendResponseEvent $event) 38 | { 39 | $response = $event->getResponse(); 40 | if (! $response instanceof Response) { 41 | return $this; 42 | } 43 | 44 | $this->sendHeaders($event) 45 | ->sendContent($event); 46 | $event->stopPropagation(true); 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-mvc 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-mvc](https://github.com/laminas/laminas-mvc). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-mvc.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-mvc) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-mvc/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-mvc?branch=master) 9 | 10 | `Zend\Mvc` is a brand new MVC implementation designed from the ground up for 11 | Zend Framework 2, focusing on performance and flexibility. 12 | 13 | The MVC layer is built on top of the following components: 14 | 15 | - `Zend\ServiceManager` - Zend Framework provides a set of default service 16 | definitions set up at `Zend\Mvc\Service`. The ServiceManager creates and 17 | configures your application instance and workflow. 18 | - `Zend\EventManager` - The MVC is event driven. This component is used 19 | everywhere from initial bootstrapping of the application, through returning 20 | response and request calls, to setting and retrieving routes and matched 21 | routes, as well as render views. 22 | - `Zend\Http` - specifically the request and response objects, used within: 23 | `Zend\Stdlib\DispatchableInterface`. All “controllers” are simply dispatchable 24 | objects. 25 | 26 | 27 | - File issues at https://github.com/zendframework/zend-mvc/issues 28 | - Documentation is at https://docs.zendframework.com/zend-mvc/ 29 | -------------------------------------------------------------------------------- /src/ResponseSender/SimpleStreamResponseSender.php: -------------------------------------------------------------------------------- 1 | contentSent()) { 23 | return $this; 24 | } 25 | $response = $event->getResponse(); 26 | $stream = $response->getStream(); 27 | fpassthru($stream); 28 | $event->setContentSent(); 29 | } 30 | 31 | /** 32 | * Send stream response 33 | * 34 | * @param SendResponseEvent $event 35 | * @return SimpleStreamResponseSender 36 | */ 37 | public function __invoke(SendResponseEvent $event) 38 | { 39 | $response = $event->getResponse(); 40 | if (! $response instanceof Stream) { 41 | return $this; 42 | } 43 | 44 | $this->sendHeaders($event); 45 | $this->sendStream($event); 46 | $event->stopPropagation(true); 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Service/ViewTemplateMapResolverFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 30 | $map = []; 31 | if (is_array($config) && isset($config['view_manager'])) { 32 | $config = $config['view_manager']; 33 | if (is_array($config) && isset($config['template_map'])) { 34 | $map = $config['template_map']; 35 | } 36 | } 37 | return new ViewResolver\TemplateMapResolver($map); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Service/ControllerManagerFactory.php: -------------------------------------------------------------------------------- 1 | get(View::class)); 28 | $config = $this->getConfig($container); 29 | 30 | $this->injectLayoutTemplate($strategy, $config); 31 | 32 | return $strategy; 33 | } 34 | 35 | /** 36 | * Inject layout template. 37 | * 38 | * Uses layout template from configuration; if none available, defaults to "layout/layout". 39 | * 40 | * @param DefaultRenderingStrategy $strategy 41 | * @param array $config 42 | */ 43 | private function injectLayoutTemplate(DefaultRenderingStrategy $strategy, array $config) 44 | { 45 | $layout = isset($config['layout']) ? $config['layout'] : 'layout/layout'; 46 | $strategy->setLayoutTemplate($layout); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Service/ViewTemplatePathStackFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 31 | 32 | $templatePathStack = new ViewResolver\TemplatePathStack(); 33 | 34 | if (is_array($config) && isset($config['view_manager'])) { 35 | $config = $config['view_manager']; 36 | if (is_array($config)) { 37 | if (isset($config['template_path_stack'])) { 38 | $templatePathStack->addPaths($config['template_path_stack']); 39 | } 40 | if (isset($config['default_template_suffix'])) { 41 | $templatePathStack->setDefaultSuffix($config['default_template_suffix']); 42 | } 43 | } 44 | } 45 | 46 | return $templatePathStack; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/View/Http/CreateViewModelListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'createViewModelFromArray'], -80); 24 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'createViewModelFromNull'], -80); 25 | } 26 | 27 | /** 28 | * Inspect the result, and cast it to a ViewModel if an assoc array is detected 29 | * 30 | * @param MvcEvent $e 31 | * @return void 32 | */ 33 | public function createViewModelFromArray(MvcEvent $e) 34 | { 35 | $result = $e->getResult(); 36 | if (! ArrayUtils::hasStringKeys($result, true)) { 37 | return; 38 | } 39 | 40 | $model = new ViewModel($result); 41 | $e->setResult($model); 42 | } 43 | 44 | /** 45 | * Inspect the result, and cast it to a ViewModel if null is detected 46 | * 47 | * @param MvcEvent $e 48 | * @return void 49 | */ 50 | public function createViewModelFromNull(MvcEvent $e) 51 | { 52 | $result = $e->getResult(); 53 | if (null !== $result) { 54 | return; 55 | } 56 | 57 | $model = new ViewModel; 58 | $e->setResult($model); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RouteListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, [$this, 'onRoute']); 26 | } 27 | 28 | /** 29 | * Listen to the "route" event and attempt to route the request 30 | * 31 | * If no matches are returned, triggers "dispatch.error" in order to 32 | * create a 404 response. 33 | * 34 | * Seeds the event with the route match on completion. 35 | * 36 | * @param MvcEvent $event 37 | * @return null|RouteMatch 38 | */ 39 | public function onRoute(MvcEvent $event) 40 | { 41 | $request = $event->getRequest(); 42 | $router = $event->getRouter(); 43 | $routeMatch = $router->match($request); 44 | 45 | if ($routeMatch instanceof RouteMatch) { 46 | $event->setRouteMatch($routeMatch); 47 | return $routeMatch; 48 | } 49 | 50 | $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); 51 | $event->setError(Application::ERROR_ROUTER_NO_MATCH); 52 | 53 | $target = $event->getTarget(); 54 | $results = $target->getEventManager()->triggerEvent($event); 55 | if (! empty($results)) { 56 | return $results->last(); 57 | } 58 | 59 | return $event->getParams(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Service/ViewResolverFactory.php: -------------------------------------------------------------------------------- 1 | get('ViewTemplateMapResolver'); 34 | /* @var $pathResolver ResolverInterface */ 35 | $pathResolver = $container->get('ViewTemplatePathStack'); 36 | /* @var $prefixPathStackResolver ResolverInterface */ 37 | $prefixPathStackResolver = $container->get('ViewPrefixPathStackResolver'); 38 | 39 | $resolver 40 | ->attach($mapResolver) 41 | ->attach($pathResolver) 42 | ->attach($prefixPathStackResolver) 43 | ->attach(new ViewResolver\RelativeFallbackResolver($mapResolver)) 44 | ->attach(new ViewResolver\RelativeFallbackResolver($pathResolver)) 45 | ->attach(new ViewResolver\RelativeFallbackResolver($prefixPathStackResolver)); 46 | 47 | return $resolver; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/View/Http/InjectViewModelListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'injectViewModel'], -100); 24 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'injectViewModel'], -100); 25 | $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$this, 'injectViewModel'], -100); 26 | } 27 | 28 | /** 29 | * Insert the view model into the event 30 | * 31 | * Inspects the MVC result; if it's a view model, it then either (a) adds 32 | * it as a child to the default, composed view model, or (b) replaces it 33 | * if the result is marked as terminable. 34 | * 35 | * @param MvcEvent $e 36 | * @return void 37 | */ 38 | public function injectViewModel(MvcEvent $e) 39 | { 40 | $result = $e->getResult(); 41 | if (! $result instanceof ViewModel) { 42 | return; 43 | } 44 | 45 | $model = $e->getViewModel(); 46 | 47 | if ($result->terminate()) { 48 | $e->setViewModel($result); 49 | return; 50 | } 51 | 52 | if ($e->getError() && $model instanceof ClearableModelInterface) { 53 | $model->clearChildren(); 54 | } 55 | 56 | $model->addChild($result); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Service/HttpExceptionStrategyFactory.php: -------------------------------------------------------------------------------- 1 | getConfig($container); 28 | 29 | $this->injectDisplayExceptions($strategy, $config); 30 | $this->injectExceptionTemplate($strategy, $config); 31 | 32 | return $strategy; 33 | } 34 | 35 | /** 36 | * Inject strategy with configured display_exceptions flag. 37 | * 38 | * @param ExceptionStrategy $strategy 39 | * @param array $config 40 | */ 41 | private function injectDisplayExceptions(ExceptionStrategy $strategy, array $config) 42 | { 43 | $flag = isset($config['display_exceptions']) ? $config['display_exceptions'] : false; 44 | $strategy->setDisplayExceptions($flag); 45 | } 46 | 47 | /** 48 | * Inject strategy with configured exception_template 49 | * 50 | * @param ExceptionStrategy $strategy 51 | * @param array $config 52 | */ 53 | private function injectExceptionTemplate(ExceptionStrategy $strategy, array $config) 54 | { 55 | $template = isset($config['exception_template']) ? $config['exception_template'] : 'error'; 56 | $strategy->setExceptionTemplate($template); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Controller/AbstractActionController.php: -------------------------------------------------------------------------------- 1 | 'Placeholder page' 33 | ]); 34 | } 35 | 36 | /** 37 | * Action called if matched action does not exist 38 | * 39 | * @return ViewModel 40 | */ 41 | public function notFoundAction() 42 | { 43 | $event = $this->getEvent(); 44 | $routeMatch = $event->getRouteMatch(); 45 | $routeMatch->setParam('action', 'not-found'); 46 | 47 | $helper = $this->plugin('createHttpNotFoundModel'); 48 | return $helper($event->getResponse()); 49 | } 50 | 51 | /** 52 | * Execute the request 53 | * 54 | * @param MvcEvent $e 55 | * @return mixed 56 | * @throws Exception\DomainException 57 | */ 58 | public function onDispatch(MvcEvent $e) 59 | { 60 | $routeMatch = $e->getRouteMatch(); 61 | if (! $routeMatch) { 62 | /** 63 | * @todo Determine requirements for when route match is missing. 64 | * Potentially allow pulling directly from request metadata? 65 | */ 66 | throw new Exception\DomainException('Missing route matches; unsure how to retrieve action'); 67 | } 68 | 69 | $action = $routeMatch->getParam('action', 'not-found'); 70 | $method = static::getMethodFromAction($action); 71 | 72 | if (! method_exists($this, $method)) { 73 | $method = 'notFoundAction'; 74 | } 75 | 76 | $actionResponse = $this->$method(); 77 | 78 | $e->setResult($actionResponse); 79 | 80 | return $actionResponse; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/View/Http/InjectRoutematchParamsListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'injectParams'], 90); 30 | } 31 | 32 | /** 33 | * Take parameters from RouteMatch and inject them into the request. 34 | * 35 | * @param MvcEvent $e 36 | * @return void 37 | */ 38 | public function injectParams(MvcEvent $e) 39 | { 40 | $routeMatchParams = $e->getRouteMatch()->getParams(); 41 | $request = $e->getRequest(); 42 | 43 | if (! $request instanceof HttpRequest) { 44 | // unsupported request type 45 | return; 46 | } 47 | 48 | $params = $request->get(); 49 | 50 | if ($this->overwrite) { 51 | // Overwrite existing parameters, or create new ones if not present. 52 | foreach ($routeMatchParams as $key => $val) { 53 | $params->$key = $val; 54 | } 55 | return; 56 | } 57 | 58 | // Only create new parameters. 59 | foreach ($routeMatchParams as $key => $val) { 60 | if (! $params->offsetExists($key)) { 61 | $params->$key = $val; 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Should RouteMatch parameters replace existing Request params? 68 | * 69 | * @param bool $overwrite 70 | */ 71 | public function setOverwrite($overwrite) 72 | { 73 | $this->overwrite = $overwrite; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function getOverwrite() 80 | { 81 | return $this->overwrite; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Service/HttpRouteNotFoundStrategyFactory.php: -------------------------------------------------------------------------------- 1 | getConfig($container); 28 | 29 | $this->injectDisplayExceptions($strategy, $config); 30 | $this->injectDisplayNotFoundReason($strategy, $config); 31 | $this->injectNotFoundTemplate($strategy, $config); 32 | 33 | return $strategy; 34 | } 35 | 36 | /** 37 | * Inject strategy with configured display_exceptions flag. 38 | * 39 | * @param RouteNotFoundStrategy $strategy 40 | * @param array $config 41 | */ 42 | private function injectDisplayExceptions(RouteNotFoundStrategy $strategy, array $config) 43 | { 44 | $flag = isset($config['display_exceptions']) ? $config['display_exceptions'] : false; 45 | $strategy->setDisplayExceptions($flag); 46 | } 47 | 48 | /** 49 | * Inject strategy with configured display_not_found_reason flag. 50 | * 51 | * @param RouteNotFoundStrategy $strategy 52 | * @param array $config 53 | */ 54 | private function injectDisplayNotFoundReason(RouteNotFoundStrategy $strategy, array $config) 55 | { 56 | $flag = isset($config['display_not_found_reason']) ? $config['display_not_found_reason'] : false; 57 | $strategy->setDisplayNotFoundReason($flag); 58 | } 59 | 60 | /** 61 | * Inject strategy with configured not_found_template. 62 | * 63 | * @param RouteNotFoundStrategy $strategy 64 | * @param array $config 65 | */ 66 | private function injectNotFoundTemplate(RouteNotFoundStrategy $strategy, array $config) 67 | { 68 | $template = isset($config['not_found_template']) ? $config['not_found_template'] : '404'; 69 | $strategy->setNotFoundTemplate($template); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ModuleRouteListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, [$this, 'onRoute'], $priority); 28 | } 29 | 30 | /** 31 | * Listen to the "route" event and determine if the module namespace should 32 | * be prepended to the controller name. 33 | * 34 | * If the route match contains a parameter key matching the MODULE_NAMESPACE 35 | * constant, that value will be prepended, with a namespace separator, to 36 | * the matched controller parameter. 37 | * 38 | * @param MvcEvent $e 39 | * @return null 40 | */ 41 | public function onRoute(MvcEvent $e) 42 | { 43 | $matches = $e->getRouteMatch(); 44 | if (! $matches instanceof RouteMatch) { 45 | // Can't do anything without a route match 46 | return; 47 | } 48 | 49 | $module = $matches->getParam(self::MODULE_NAMESPACE, false); 50 | if (! $module) { 51 | // No module namespace found; nothing to do 52 | return; 53 | } 54 | 55 | $controller = $matches->getParam('controller', false); 56 | if (! $controller) { 57 | // no controller matched, nothing to do 58 | return; 59 | } 60 | 61 | // Ensure the module namespace has not already been applied 62 | if (0 === strpos($controller, $module)) { 63 | return; 64 | } 65 | 66 | // Keep the originally matched controller name around 67 | $matches->setParam(self::ORIGINAL_CONTROLLER, $controller); 68 | 69 | // Prepend the controllername with the module, and replace it in the 70 | // matches 71 | $controller = $module . '\\' . str_replace(' ', '', ucwords(str_replace('-', ' ', $controller))); 72 | $matches->setParam('controller', $controller); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Controller/Plugin/Layout.php: -------------------------------------------------------------------------------- 1 | getViewModel(); 31 | $viewModel->setTemplate((string) $template); 32 | return $this; 33 | } 34 | 35 | /** 36 | * Invoke as a functor 37 | * 38 | * If no arguments are given, grabs the "root" or "layout" view model. 39 | * Otherwise, attempts to set the template for that view model. 40 | * 41 | * @param null|string $template 42 | * @return Model|Layout 43 | */ 44 | public function __invoke($template = null) 45 | { 46 | if (null === $template) { 47 | return $this->getViewModel(); 48 | } 49 | return $this->setTemplate($template); 50 | } 51 | 52 | /** 53 | * Get the event 54 | * 55 | * @return MvcEvent 56 | * @throws Exception\DomainException if unable to find event 57 | */ 58 | protected function getEvent() 59 | { 60 | if ($this->event) { 61 | return $this->event; 62 | } 63 | 64 | $controller = $this->getController(); 65 | if (! $controller instanceof InjectApplicationEventInterface) { 66 | throw new Exception\DomainException( 67 | 'Layout plugin requires a controller that implements InjectApplicationEventInterface' 68 | ); 69 | } 70 | 71 | $event = $controller->getEvent(); 72 | if (! $event instanceof MvcEvent) { 73 | $params = $event->getParams(); 74 | $event = new MvcEvent(); 75 | $event->setParams($params); 76 | } 77 | $this->event = $event; 78 | 79 | return $this->event; 80 | } 81 | 82 | /** 83 | * Retrieve the root view model from the event 84 | * 85 | * @return Model 86 | * @throws Exception\DomainException 87 | */ 88 | protected function getViewModel() 89 | { 90 | $event = $this->getEvent(); 91 | $viewModel = $event->getViewModel(); 92 | if (! $viewModel instanceof Model) { 93 | throw new Exception\DomainException('Layout plugin requires that event view model is populated'); 94 | } 95 | return $viewModel; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ResponseSender/SendResponseEvent.php: -------------------------------------------------------------------------------- 1 | setParam('response', $response); 50 | $this->response = $response; 51 | return $this; 52 | } 53 | 54 | /** 55 | * @return \Zend\Stdlib\ResponseInterface 56 | */ 57 | public function getResponse() 58 | { 59 | return $this->response; 60 | } 61 | 62 | /** 63 | * Set content sent for current response 64 | * 65 | * @return SendResponseEvent 66 | */ 67 | public function setContentSent() 68 | { 69 | $response = $this->getResponse(); 70 | $contentSent = $this->getParam('contentSent', []); 71 | $responseObjectHash = spl_object_hash($response); 72 | $contentSent[$responseObjectHash] = true; 73 | $this->setParam('contentSent', $contentSent); 74 | $this->contentSent[$responseObjectHash] = true; 75 | return $this; 76 | } 77 | 78 | /** 79 | * @return bool 80 | */ 81 | public function contentSent() 82 | { 83 | $response = $this->getResponse(); 84 | if (isset($this->contentSent[spl_object_hash($response)])) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | /** 91 | * Set headers sent for current response object 92 | * 93 | * @return SendResponseEvent 94 | */ 95 | public function setHeadersSent() 96 | { 97 | $response = $this->getResponse(); 98 | $headersSent = $this->getParam('headersSent', []); 99 | $responseObjectHash = spl_object_hash($response); 100 | $headersSent[$responseObjectHash] = true; 101 | $this->setParam('headersSent', $headersSent); 102 | $this->headersSent[$responseObjectHash] = true; 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return bool 108 | */ 109 | public function headersSent() 110 | { 111 | $response = $this->getResponse(); 112 | if (isset($this->headersSent[spl_object_hash($response)])) { 113 | return true; 114 | } 115 | return false; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/HttpMethodListener.php: -------------------------------------------------------------------------------- 1 | setEnabled($enabled); 45 | 46 | if (! empty($allowedMethods)) { 47 | $this->setAllowedMethods($allowedMethods); 48 | } 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function attach(EventManagerInterface $events, $priority = 1) 55 | { 56 | if (! $this->isEnabled()) { 57 | return; 58 | } 59 | 60 | $this->listeners[] = $events->attach( 61 | MvcEvent::EVENT_ROUTE, 62 | [$this, 'onRoute'], 63 | 10000 64 | ); 65 | } 66 | 67 | /** 68 | * @param MvcEvent $e 69 | * @return void|HttpResponse 70 | */ 71 | public function onRoute(MvcEvent $e) 72 | { 73 | $request = $e->getRequest(); 74 | $response = $e->getResponse(); 75 | 76 | if (! $request instanceof HttpRequest || ! $response instanceof HttpResponse) { 77 | return; 78 | } 79 | 80 | $method = $request->getMethod(); 81 | 82 | if (in_array($method, $this->getAllowedMethods())) { 83 | return; 84 | } 85 | 86 | $response->setStatusCode(405); 87 | 88 | return $response; 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function getAllowedMethods() 95 | { 96 | return $this->allowedMethods; 97 | } 98 | 99 | /** 100 | * @param array $allowedMethods 101 | */ 102 | public function setAllowedMethods(array $allowedMethods) 103 | { 104 | foreach ($allowedMethods as &$value) { 105 | $value = strtoupper($value); 106 | } 107 | $this->allowedMethods = $allowedMethods; 108 | } 109 | 110 | /** 111 | * @return bool 112 | */ 113 | public function isEnabled() 114 | { 115 | return $this->enabled; 116 | } 117 | 118 | /** 119 | * @param bool $enabled 120 | */ 121 | public function setEnabled($enabled) 122 | { 123 | $this->enabled = (bool) $enabled; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Service/ModuleManagerFactory.php: -------------------------------------------------------------------------------- 1 | get('ApplicationConfig'); 38 | $listenerOptions = new ListenerOptions($configuration['module_listener_options']); 39 | $defaultListeners = new DefaultListenerAggregate($listenerOptions); 40 | $serviceListener = $container->get('ServiceListener'); 41 | 42 | $serviceListener->addServiceManager( 43 | $container, 44 | 'service_manager', 45 | 'Zend\ModuleManager\Feature\ServiceProviderInterface', 46 | 'getServiceConfig' 47 | ); 48 | 49 | $serviceListener->addServiceManager( 50 | 'ControllerManager', 51 | 'controllers', 52 | 'Zend\ModuleManager\Feature\ControllerProviderInterface', 53 | 'getControllerConfig' 54 | ); 55 | $serviceListener->addServiceManager( 56 | 'ControllerPluginManager', 57 | 'controller_plugins', 58 | 'Zend\ModuleManager\Feature\ControllerPluginProviderInterface', 59 | 'getControllerPluginConfig' 60 | ); 61 | $serviceListener->addServiceManager( 62 | 'ViewHelperManager', 63 | 'view_helpers', 64 | 'Zend\ModuleManager\Feature\ViewHelperProviderInterface', 65 | 'getViewHelperConfig' 66 | ); 67 | $serviceListener->addServiceManager( 68 | 'RoutePluginManager', 69 | 'route_manager', 70 | 'Zend\ModuleManager\Feature\RouteProviderInterface', 71 | 'getRouteConfig' 72 | ); 73 | 74 | $events = $container->get('EventManager'); 75 | $defaultListeners->attach($events); 76 | $serviceListener->attach($events); 77 | 78 | $moduleEvent = new ModuleEvent; 79 | $moduleEvent->setParam('ServiceManager', $container); 80 | 81 | $moduleManager = new ModuleManager($configuration['modules'], $events); 82 | $moduleManager->setEvent($moduleEvent); 83 | 84 | return $moduleManager; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/View/Http/DefaultRenderingStrategy.php: -------------------------------------------------------------------------------- 1 | view = $view; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function attach(EventManagerInterface $events, $priority = 1) 47 | { 48 | $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, [$this, 'render'], -10000); 49 | $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$this, 'render'], -10000); 50 | } 51 | 52 | /** 53 | * Set layout template value 54 | * 55 | * @param string $layoutTemplate 56 | * @return DefaultRenderingStrategy 57 | */ 58 | public function setLayoutTemplate($layoutTemplate) 59 | { 60 | $this->layoutTemplate = (string) $layoutTemplate; 61 | return $this; 62 | } 63 | 64 | /** 65 | * Get layout template value 66 | * 67 | * @return string 68 | */ 69 | public function getLayoutTemplate() 70 | { 71 | return $this->layoutTemplate; 72 | } 73 | 74 | /** 75 | * Render the view 76 | * 77 | * @param MvcEvent $e 78 | * @return Response|null 79 | * @throws \Exception|\Throwable 80 | */ 81 | public function render(MvcEvent $e) 82 | { 83 | $result = $e->getResult(); 84 | if ($result instanceof Response) { 85 | return $result; 86 | } 87 | 88 | // Martial arguments 89 | $request = $e->getRequest(); 90 | $response = $e->getResponse(); 91 | $viewModel = $e->getViewModel(); 92 | if (! $viewModel instanceof ViewModel) { 93 | return; 94 | } 95 | 96 | $view = $this->view; 97 | $view->setRequest($request); 98 | $view->setResponse($response); 99 | 100 | $caughtException = null; 101 | 102 | try { 103 | $view->render($viewModel); 104 | } catch (\Throwable $ex) { 105 | $caughtException = $ex; 106 | } catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced 107 | $caughtException = $ex; 108 | } 109 | 110 | if ($caughtException !== null) { 111 | if ($e->getName() === MvcEvent::EVENT_RENDER_ERROR) { 112 | throw $caughtException; 113 | } 114 | 115 | $application = $e->getApplication(); 116 | $events = $application->getEventManager(); 117 | 118 | $e->setError(Application::ERROR_EXCEPTION); 119 | $e->setParam('exception', $caughtException); 120 | $e->setName(MvcEvent::EVENT_RENDER_ERROR); 121 | $events->triggerEvent($e); 122 | } 123 | 124 | return $response; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Controller/ControllerManager.php: -------------------------------------------------------------------------------- 1 | addInitializer([$this, 'injectEventManager']); 51 | $this->addInitializer([$this, 'injectPluginManager']); 52 | parent::__construct($configOrContainerInstance, $config); 53 | } 54 | 55 | /** 56 | * Validate a plugin 57 | * 58 | * {@inheritDoc} 59 | */ 60 | public function validate($plugin) 61 | { 62 | if (! $plugin instanceof $this->instanceOf) { 63 | throw new InvalidServiceException(sprintf( 64 | 'Plugin of type "%s" is invalid; must implement %s', 65 | (is_object($plugin) ? get_class($plugin) : gettype($plugin)), 66 | $this->instanceOf 67 | )); 68 | } 69 | } 70 | 71 | /** 72 | * Initializer: inject EventManager instance 73 | * 74 | * If we have an event manager composed already, make sure it gets injected 75 | * with the shared event manager. 76 | * 77 | * The AbstractController lazy-instantiates an EM instance, which is why 78 | * the shared EM injection needs to happen; the conditional will always 79 | * pass. 80 | * 81 | * @param ContainerInterface $container 82 | * @param DispatchableInterface $controller 83 | */ 84 | public function injectEventManager(ContainerInterface $container, $controller) 85 | { 86 | if (! $controller instanceof EventManagerAwareInterface) { 87 | return; 88 | } 89 | 90 | $events = $controller->getEventManager(); 91 | if (! $events || ! $events->getSharedManager() instanceof SharedEventManagerInterface) { 92 | $controller->setEventManager($container->get('EventManager')); 93 | } 94 | } 95 | 96 | /** 97 | * Initializer: inject plugin manager 98 | * 99 | * @param ContainerInterface $container 100 | * @param DispatchableInterface $controller 101 | */ 102 | public function injectPluginManager(ContainerInterface $container, $controller) 103 | { 104 | if (! method_exists($controller, 'setPluginManager')) { 105 | return; 106 | } 107 | 108 | $controller->setPluginManager($container->get('ControllerPluginManager')); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-mvc", 3 | "description": "Zend Framework's event-driven MVC layer, including MVC Applications, Controllers, and Plugins", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "mvc" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-mvc/", 12 | "issues": "https://github.com/zendframework/zend-mvc/issues", 13 | "source": "https://github.com/zendframework/zend-mvc", 14 | "rss": "https://github.com/zendframework/zend-mvc/releases.atom", 15 | "slack": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "container-interop/container-interop": "^1.2", 20 | "php": "^5.6 || ^7.0", 21 | "zendframework/zend-eventmanager": "^3.2", 22 | "zendframework/zend-http": "^2.7", 23 | "zendframework/zend-modulemanager": "^2.8", 24 | "zendframework/zend-router": "^3.0.2", 25 | "zendframework/zend-servicemanager": "^3.3", 26 | "zendframework/zend-stdlib": "^3.1", 27 | "zendframework/zend-view": "^2.9" 28 | }, 29 | "require-dev": { 30 | "http-interop/http-middleware": "^0.4.1", 31 | "phpunit/phpunit": "^6.4.4 || ^5.7.14", 32 | "zendframework/zend-coding-standard": "~1.0.0", 33 | "zendframework/zend-json": "^2.6.1 || ^3.0", 34 | "zendframework/zend-psr7bridge": "^1.0", 35 | "zendframework/zend-stratigility": ">=2.0.1 <2.2" 36 | }, 37 | "suggest": { 38 | "zendframework/zend-json": "(^2.6.1 || ^3.0) To auto-deserialize JSON body content in AbstractRestfulController extensions, when json_decode is unavailable", 39 | "zendframework/zend-log": "^2.9.1 To provide log functionality via LogFilterManager, LogFormatterManager, and LogProcessorManager", 40 | "zendframework/zend-mvc-console": "zend-mvc-console provides the ability to expose zend-mvc as a console application", 41 | "zendframework/zend-mvc-i18n": "zend-mvc-i18n provides integration with zend-i18n, including a translation bridge and translatable route segments", 42 | "zendframework/zend-mvc-plugin-fileprg": "To provide Post/Redirect/Get functionality around forms that container file uploads", 43 | "zendframework/zend-mvc-plugin-flashmessenger": "To provide flash messaging capabilities between requests", 44 | "zendframework/zend-mvc-plugin-identity": "To access the authenticated identity (per zend-authentication) in controllers", 45 | "zendframework/zend-mvc-plugin-prg": "To provide Post/Redirect/Get functionality within controllers", 46 | "zendframework/zend-paginator": "^2.7 To provide pagination functionality via PaginatorPluginManager", 47 | "zendframework/zend-psr7bridge": "(^0.2) To consume PSR-7 middleware within the MVC workflow", 48 | "zendframework/zend-servicemanager-di": "zend-servicemanager-di provides utilities for integrating zend-di and zend-servicemanager in your zend-mvc application", 49 | "zendframework/zend-stratigility": "(>=2.0.1 <2.2) zend-stratigility is required to use middleware pipes in the MiddlewareListener", 50 | "http-interop/http-middleware": "^0.4.1 to be used together with zend-stratigility" 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "Zend\\Mvc\\": "src/" 55 | } 56 | }, 57 | "autoload-dev": { 58 | "psr-4": { 59 | "ZendTest\\Mvc\\": "test/" 60 | }, 61 | "files": [ 62 | "test/_autoload.php" 63 | ] 64 | }, 65 | "config": { 66 | "sort-packages": true 67 | }, 68 | "extra": { 69 | "branch-alias": { 70 | "dev-master": "3.1-dev", 71 | "dev-develop": "3.2-dev" 72 | } 73 | }, 74 | "scripts": { 75 | "check": [ 76 | "@cs-check", 77 | "@test" 78 | ], 79 | "cs-check": "phpcs", 80 | "cs-fix": "phpcbf", 81 | "test": "phpunit --colors=always", 82 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Controller/MiddlewareController.php: -------------------------------------------------------------------------------- 1 | eventIdentifier = __CLASS__; 53 | $this->pipe = $pipe; 54 | $this->responsePrototype = $responsePrototype; 55 | 56 | $this->setEventManager($eventManager); 57 | $this->setEvent($event); 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | * 63 | * @throws RuntimeException 64 | */ 65 | public function onDispatch(MvcEvent $e) 66 | { 67 | $routeMatch = $e->getRouteMatch(); 68 | $psr7Request = $this->populateRequestParametersFromRoute( 69 | $this->loadRequest()->withAttribute(RouteMatch::class, $routeMatch), 70 | $routeMatch 71 | ); 72 | 73 | $result = $this->pipe->process($psr7Request, new CallableDelegateDecorator( 74 | function () { 75 | throw ReachedFinalHandlerException::create(); 76 | }, 77 | $this->responsePrototype 78 | )); 79 | 80 | $e->setResult($result); 81 | 82 | return $result; 83 | } 84 | 85 | /** 86 | * @return \Zend\Diactoros\ServerRequest 87 | * 88 | * @throws RuntimeException 89 | */ 90 | private function loadRequest() 91 | { 92 | $request = $this->request; 93 | 94 | if (! $request instanceof Request) { 95 | throw new RuntimeException(sprintf( 96 | 'Expected request to be a %s, %s given', 97 | Request::class, 98 | get_class($request) 99 | )); 100 | } 101 | 102 | return Psr7ServerRequest::fromZend($request); 103 | } 104 | 105 | /** 106 | * @param ServerRequestInterface $request 107 | * @param RouteMatch|null $routeMatch 108 | * 109 | * @return ServerRequestInterface 110 | */ 111 | private function populateRequestParametersFromRoute(ServerRequestInterface $request, RouteMatch $routeMatch = null) 112 | { 113 | if (! $routeMatch) { 114 | return $request; 115 | } 116 | 117 | foreach ($routeMatch->getParams() as $key => $value) { 118 | $request = $request->withAttribute($key, $value); 119 | } 120 | 121 | return $request; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Controller/Plugin/Url.php: -------------------------------------------------------------------------------- 1 | getController(); 37 | if (! $controller instanceof InjectApplicationEventInterface) { 38 | throw new Exception\DomainException( 39 | 'Url plugin requires a controller that implements InjectApplicationEventInterface' 40 | ); 41 | } 42 | 43 | if (! is_array($params)) { 44 | if (! $params instanceof Traversable) { 45 | throw new Exception\InvalidArgumentException( 46 | 'Params is expected to be an array or a Traversable object' 47 | ); 48 | } 49 | $params = iterator_to_array($params); 50 | } 51 | 52 | $event = $controller->getEvent(); 53 | $router = null; 54 | $matches = null; 55 | if ($event instanceof MvcEvent) { 56 | $router = $event->getRouter(); 57 | $matches = $event->getRouteMatch(); 58 | } elseif ($event instanceof EventInterface) { 59 | $router = $event->getParam('router', false); 60 | $matches = $event->getParam('route-match', false); 61 | } 62 | if (! $router instanceof RouteStackInterface) { 63 | throw new Exception\DomainException( 64 | 'Url plugin requires that controller event compose a router; none found' 65 | ); 66 | } 67 | 68 | if (3 == func_num_args() && is_bool($options)) { 69 | $reuseMatchedParams = $options; 70 | $options = []; 71 | } 72 | 73 | if ($route === null) { 74 | if (! $matches) { 75 | throw new Exception\RuntimeException('No RouteMatch instance present'); 76 | } 77 | 78 | $route = $matches->getMatchedRouteName(); 79 | 80 | if ($route === null) { 81 | throw new Exception\RuntimeException('RouteMatch does not contain a matched route name'); 82 | } 83 | } 84 | 85 | if ($reuseMatchedParams && $matches) { 86 | $routeMatchParams = $matches->getParams(); 87 | 88 | if (isset($routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER])) { 89 | $routeMatchParams['controller'] = $routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER]; 90 | unset($routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER]); 91 | } 92 | 93 | if (isset($routeMatchParams[ModuleRouteListener::MODULE_NAMESPACE])) { 94 | unset($routeMatchParams[ModuleRouteListener::MODULE_NAMESPACE]); 95 | } 96 | 97 | $params = array_merge($routeMatchParams, $params); 98 | } 99 | 100 | $options['name'] = $route; 101 | return $router->assemble($params, $options); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Controller/Plugin/Redirect.php: -------------------------------------------------------------------------------- 1 | getController(); 37 | if (! $controller || ! method_exists($controller, 'plugin')) { 38 | throw new Exception\DomainException( 39 | 'Redirect plugin requires a controller that defines the plugin() method' 40 | ); 41 | } 42 | 43 | $urlPlugin = $controller->plugin('url'); 44 | 45 | if (is_scalar($options)) { 46 | $url = $urlPlugin->fromRoute($route, $params, $options); 47 | } else { 48 | $url = $urlPlugin->fromRoute($route, $params, $options, $reuseMatchedParams); 49 | } 50 | 51 | return $this->toUrl($url); 52 | } 53 | 54 | /** 55 | * Generate redirect response based on given URL 56 | * 57 | * @param string $url 58 | * @return Response 59 | */ 60 | public function toUrl($url) 61 | { 62 | $response = $this->getResponse(); 63 | $response->getHeaders()->addHeaderLine('Location', $url); 64 | $response->setStatusCode(302); 65 | return $response; 66 | } 67 | 68 | /** 69 | * Refresh to current route 70 | * 71 | * @return Response 72 | */ 73 | public function refresh() 74 | { 75 | return $this->toRoute(null, [], [], true); 76 | } 77 | 78 | /** 79 | * Get the response 80 | * 81 | * @return Response 82 | * @throws Exception\DomainException if unable to find response 83 | */ 84 | protected function getResponse() 85 | { 86 | if ($this->response) { 87 | return $this->response; 88 | } 89 | 90 | $event = $this->getEvent(); 91 | $response = $event->getResponse(); 92 | if (! $response instanceof Response) { 93 | throw new Exception\DomainException('Redirect plugin requires event compose a response'); 94 | } 95 | $this->response = $response; 96 | return $this->response; 97 | } 98 | 99 | /** 100 | * Get the event 101 | * 102 | * @return MvcEvent 103 | * @throws Exception\DomainException if unable to find event 104 | */ 105 | protected function getEvent() 106 | { 107 | if ($this->event) { 108 | return $this->event; 109 | } 110 | 111 | $controller = $this->getController(); 112 | if (! $controller instanceof InjectApplicationEventInterface) { 113 | throw new Exception\DomainException( 114 | 'Redirect plugin requires a controller that implements InjectApplicationEventInterface' 115 | ); 116 | } 117 | 118 | $event = $controller->getEvent(); 119 | if (! $event instanceof MvcEvent) { 120 | $params = $event->getParams(); 121 | $event = new MvcEvent(); 122 | $event->setParams($params); 123 | } 124 | $this->event = $event; 125 | 126 | return $this->event; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Controller/Plugin/Params.php: -------------------------------------------------------------------------------- 1 | fromRoute($param, $default); 28 | } 29 | 30 | /** 31 | * Return all files or a single file. 32 | * 33 | * @param string $name File name to retrieve, or null to get all. 34 | * @param mixed $default Default value to use when the file is missing. 35 | * @return array|\ArrayAccess|null 36 | */ 37 | public function fromFiles($name = null, $default = null) 38 | { 39 | if ($name === null) { 40 | return $this->getController()->getRequest()->getFiles($name, $default)->toArray(); 41 | } 42 | 43 | return $this->getController()->getRequest()->getFiles($name, $default); 44 | } 45 | 46 | /** 47 | * Return all header parameters or a single header parameter. 48 | * 49 | * @param string $header Header name to retrieve, or null to get all. 50 | * @param mixed $default Default value to use when the requested header is missing. 51 | * @return null|\Zend\Http\Header\HeaderInterface 52 | */ 53 | public function fromHeader($header = null, $default = null) 54 | { 55 | if ($header === null) { 56 | return $this->getController()->getRequest()->getHeaders($header, $default)->toArray(); 57 | } 58 | 59 | return $this->getController()->getRequest()->getHeaders($header, $default); 60 | } 61 | 62 | /** 63 | * Return all post parameters or a single post parameter. 64 | * 65 | * @param string $param Parameter name to retrieve, or null to get all. 66 | * @param mixed $default Default value to use when the parameter is missing. 67 | * @return mixed 68 | */ 69 | public function fromPost($param = null, $default = null) 70 | { 71 | if ($param === null) { 72 | return $this->getController()->getRequest()->getPost($param, $default)->toArray(); 73 | } 74 | 75 | return $this->getController()->getRequest()->getPost($param, $default); 76 | } 77 | 78 | /** 79 | * Return all query parameters or a single query parameter. 80 | * 81 | * @param string $param Parameter name to retrieve, or null to get all. 82 | * @param mixed $default Default value to use when the parameter is missing. 83 | * @return mixed 84 | */ 85 | public function fromQuery($param = null, $default = null) 86 | { 87 | if ($param === null) { 88 | return $this->getController()->getRequest()->getQuery($param, $default)->toArray(); 89 | } 90 | 91 | return $this->getController()->getRequest()->getQuery($param, $default); 92 | } 93 | 94 | /** 95 | * Return all route parameters or a single route parameter. 96 | * 97 | * @param string $param Parameter name to retrieve, or null to get all. 98 | * @param mixed $default Default value to use when the parameter is missing. 99 | * @return mixed 100 | * @throws RuntimeException 101 | */ 102 | public function fromRoute($param = null, $default = null) 103 | { 104 | $controller = $this->getController(); 105 | 106 | if (! $controller instanceof InjectApplicationEventInterface) { 107 | throw new RuntimeException( 108 | 'Controllers must implement Zend\Mvc\InjectApplicationEventInterface to use this plugin.' 109 | ); 110 | } 111 | 112 | if ($param === null) { 113 | return $controller->getEvent()->getRouteMatch()->getParams(); 114 | } 115 | 116 | return $controller->getEvent()->getRouteMatch()->getParam($param, $default); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/SendResponseListener.php: -------------------------------------------------------------------------------- 1 | setIdentifiers([ 42 | __CLASS__, 43 | get_class($this), 44 | ]); 45 | $this->eventManager = $eventManager; 46 | $this->attachDefaultListeners(); 47 | return $this; 48 | } 49 | 50 | /** 51 | * Retrieve the event manager 52 | * 53 | * Lazy-loads an EventManager instance if none registered. 54 | * 55 | * @return EventManagerInterface 56 | */ 57 | public function getEventManager() 58 | { 59 | if (! $this->eventManager instanceof EventManagerInterface) { 60 | $this->setEventManager(new EventManager()); 61 | } 62 | return $this->eventManager; 63 | } 64 | 65 | /** 66 | * Attach the aggregate to the specified event manager 67 | * 68 | * @param EventManagerInterface $events 69 | * @param int $priority 70 | * @return void 71 | */ 72 | public function attach(EventManagerInterface $events, $priority = 1) 73 | { 74 | $this->listeners[] = $events->attach(MvcEvent::EVENT_FINISH, [$this, 'sendResponse'], -10000); 75 | } 76 | 77 | /** 78 | * Send the response 79 | * 80 | * @param MvcEvent $e 81 | * @return void 82 | */ 83 | public function sendResponse(MvcEvent $e) 84 | { 85 | $response = $e->getResponse(); 86 | if (! $response instanceof Response) { 87 | return; // there is no response to send 88 | } 89 | $event = $this->getEvent(); 90 | $event->setResponse($response); 91 | $event->setTarget($this); 92 | $this->getEventManager()->triggerEvent($event); 93 | } 94 | 95 | /** 96 | * Get the send response event 97 | * 98 | * @return SendResponseEvent 99 | */ 100 | public function getEvent() 101 | { 102 | if (! $this->event instanceof SendResponseEvent) { 103 | $this->setEvent(new SendResponseEvent()); 104 | } 105 | return $this->event; 106 | } 107 | 108 | /** 109 | * Set the send response event 110 | * 111 | * @param SendResponseEvent $e 112 | * @return SendResponseEvent 113 | */ 114 | public function setEvent(SendResponseEvent $e) 115 | { 116 | $this->event = $e; 117 | return $this; 118 | } 119 | 120 | /** 121 | * Register the default event listeners 122 | * 123 | * The order in which the response sender are listed here, is by their usage: 124 | * PhpEnvironmentResponseSender has highest priority, because it's used most often. 125 | * SimpleStreamResponseSender is not used that often, so has a lower priority. 126 | * You can attach your response sender before or after every default response sender implementation. 127 | * All default response sender implementation have negative priority. 128 | * You are able to attach listeners without giving a priority and your response sender would be first to try. 129 | * 130 | * @return SendResponseListener 131 | */ 132 | protected function attachDefaultListeners() 133 | { 134 | $events = $this->getEventManager(); 135 | $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new PhpEnvironmentResponseSender(), -1000); 136 | $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new SimpleStreamResponseSender(), -3000); 137 | $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new HttpResponseSender(), -4000); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/View/Http/ExceptionStrategy.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'prepareExceptionViewModel']); 38 | $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$this, 'prepareExceptionViewModel']); 39 | } 40 | 41 | /** 42 | * Flag: display exceptions in error pages? 43 | * 44 | * @param bool $displayExceptions 45 | * @return ExceptionStrategy 46 | */ 47 | public function setDisplayExceptions($displayExceptions) 48 | { 49 | $this->displayExceptions = (bool) $displayExceptions; 50 | return $this; 51 | } 52 | 53 | /** 54 | * Should we display exceptions in error pages? 55 | * 56 | * @return bool 57 | */ 58 | public function displayExceptions() 59 | { 60 | return $this->displayExceptions; 61 | } 62 | 63 | /** 64 | * Set the exception template 65 | * 66 | * @param string $exceptionTemplate 67 | * @return ExceptionStrategy 68 | */ 69 | public function setExceptionTemplate($exceptionTemplate) 70 | { 71 | $this->exceptionTemplate = (string) $exceptionTemplate; 72 | return $this; 73 | } 74 | 75 | /** 76 | * Retrieve the exception template 77 | * 78 | * @return string 79 | */ 80 | public function getExceptionTemplate() 81 | { 82 | return $this->exceptionTemplate; 83 | } 84 | 85 | /** 86 | * Create an exception view model, and set the HTTP status code 87 | * 88 | * @todo dispatch.error does not halt dispatch unless a response is 89 | * returned. As such, we likely need to trigger rendering as a low 90 | * priority dispatch.error event (or goto a render event) to ensure 91 | * rendering occurs, and that munging of view models occurs when 92 | * expected. 93 | * @param MvcEvent $e 94 | * @return void 95 | */ 96 | public function prepareExceptionViewModel(MvcEvent $e) 97 | { 98 | // Do nothing if no error in the event 99 | $error = $e->getError(); 100 | if (empty($error)) { 101 | return; 102 | } 103 | 104 | // Do nothing if the result is a response object 105 | $result = $e->getResult(); 106 | if ($result instanceof Response) { 107 | return; 108 | } 109 | 110 | switch ($error) { 111 | case Application::ERROR_CONTROLLER_NOT_FOUND: 112 | case Application::ERROR_CONTROLLER_INVALID: 113 | case Application::ERROR_ROUTER_NO_MATCH: 114 | // Specifically not handling these 115 | return; 116 | 117 | case Application::ERROR_EXCEPTION: 118 | default: 119 | $model = new ViewModel([ 120 | 'message' => 'An error occurred during execution; please try again later.', 121 | 'exception' => $e->getParam('exception'), 122 | 'display_exceptions' => $this->displayExceptions(), 123 | ]); 124 | $model->setTemplate($this->getExceptionTemplate()); 125 | $e->setResult($model); 126 | 127 | $response = $e->getResponse(); 128 | if (! $response) { 129 | $response = new HttpResponse(); 130 | $response->setStatusCode(500); 131 | $e->setResponse($response); 132 | } else { 133 | $statusCode = $response->getStatusCode(); 134 | if ($statusCode === 200) { 135 | $response->setStatusCode(500); 136 | } 137 | } 138 | 139 | break; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Service/ServiceManagerConfig.php: -------------------------------------------------------------------------------- 1 | [], 35 | 'aliases' => [ 36 | 'EventManagerInterface' => EventManager::class, 37 | EventManagerInterface::class => 'EventManager', 38 | ModuleManager::class => 'ModuleManager', 39 | ServiceListener::class => 'ServiceListener', 40 | SharedEventManager::class => 'SharedEventManager', 41 | 'SharedEventManagerInterface' => 'SharedEventManager', 42 | SharedEventManagerInterface::class => 'SharedEventManager', 43 | ], 44 | 'delegators' => [], 45 | 'factories' => [ 46 | 'EventManager' => EventManagerFactory::class, 47 | 'ModuleManager' => ModuleManagerFactory::class, 48 | 'ServiceListener' => ServiceListenerFactory::class, 49 | ], 50 | 'lazy_services' => [], 51 | 'initializers' => [], 52 | 'invokables' => [], 53 | 'services' => [], 54 | 'shared' => [ 55 | 'EventManager' => false, 56 | ], 57 | ]; 58 | 59 | /** 60 | * Constructor 61 | * 62 | * Merges internal arrays with those passed via configuration, and also 63 | * defines: 64 | * 65 | * - factory for the service 'SharedEventManager'. 66 | * - initializer for EventManagerAwareInterface implementations 67 | * 68 | * @param array $config 69 | */ 70 | public function __construct(array $config = []) 71 | { 72 | $this->config['factories']['ServiceManager'] = function ($container) { 73 | return $container; 74 | }; 75 | 76 | $this->config['factories']['SharedEventManager'] = function () { 77 | return new SharedEventManager(); 78 | }; 79 | 80 | $this->config['initializers'] = ArrayUtils::merge($this->config['initializers'], [ 81 | 'EventManagerAwareInitializer' => function ($first, $second) { 82 | if ($first instanceof ContainerInterface) { 83 | $container = $first; 84 | $instance = $second; 85 | } else { 86 | $container = $second; 87 | $instance = $first; 88 | } 89 | 90 | if (! $instance instanceof EventManagerAwareInterface) { 91 | return; 92 | } 93 | 94 | $eventManager = $instance->getEventManager(); 95 | 96 | // If the instance has an EM WITH an SEM composed, do nothing. 97 | if ($eventManager instanceof EventManagerInterface 98 | && $eventManager->getSharedManager() instanceof SharedEventManagerInterface 99 | ) { 100 | return; 101 | } 102 | 103 | $instance->setEventManager($container->get('EventManager')); 104 | }, 105 | ]); 106 | 107 | parent::__construct($config); 108 | } 109 | 110 | /** 111 | * Configure service container. 112 | * 113 | * Uses the configuration present in the instance to configure the provided 114 | * service container. 115 | * 116 | * Before doing so, it adds a "service" entry for the ServiceManager class, 117 | * pointing to the provided service container. 118 | * 119 | * @param ServiceManager $services 120 | * @return ServiceManager 121 | */ 122 | public function configureServiceManager(ServiceManager $services) 123 | { 124 | $this->config['services'][ServiceManager::class] = $services; 125 | 126 | // This is invoked as part of the bootstrapping process, and requires 127 | // the ability to override services. 128 | $services->setAllowOverride(true); 129 | parent::configureServiceManager($services); 130 | $services->setAllowOverride(false); 131 | 132 | return $services; 133 | } 134 | 135 | /** 136 | * Return all service configuration 137 | * 138 | * @return array 139 | */ 140 | public function toArray() 141 | { 142 | return $this->config; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Service/ViewHelperManagerFactory.php: -------------------------------------------------------------------------------- 1 | injectOverrideFactories($plugins, $container); 47 | 48 | return $plugins; 49 | } 50 | 51 | /** 52 | * Inject override factories into the plugin manager. 53 | * 54 | * @param HelperPluginManager $plugins 55 | * @param ContainerInterface $services 56 | * @return HelperPluginManager 57 | */ 58 | private function injectOverrideFactories(HelperPluginManager $plugins, ContainerInterface $services) 59 | { 60 | // Configure URL view helper 61 | $urlFactory = $this->createUrlHelperFactory($services); 62 | $plugins->setFactory(ViewHelper\Url::class, $urlFactory); 63 | $plugins->setFactory('zendviewhelperurl', $urlFactory); 64 | 65 | // Configure base path helper 66 | $basePathFactory = $this->createBasePathHelperFactory($services); 67 | $plugins->setFactory(ViewHelper\BasePath::class, $basePathFactory); 68 | $plugins->setFactory('zendviewhelperbasepath', $basePathFactory); 69 | 70 | // Configure doctype view helper 71 | $doctypeFactory = $this->createDoctypeHelperFactory($services); 72 | $plugins->setFactory(ViewHelper\Doctype::class, $doctypeFactory); 73 | $plugins->setFactory('zendviewhelperdoctype', $doctypeFactory); 74 | 75 | return $plugins; 76 | } 77 | 78 | /** 79 | * Create and return a factory for creating a URL helper. 80 | * 81 | * Retrieves the application and router from the servicemanager, 82 | * and the route match from the MvcEvent composed by the application, 83 | * using them to configure the helper. 84 | * 85 | * @param ContainerInterface $services 86 | * @return callable 87 | */ 88 | private function createUrlHelperFactory(ContainerInterface $services) 89 | { 90 | return function () use ($services) { 91 | $helper = new ViewHelper\Url; 92 | $helper->setRouter($services->get('HttpRouter')); 93 | 94 | $match = $services->get('Application') 95 | ->getMvcEvent() 96 | ->getRouteMatch() 97 | ; 98 | 99 | if ($match instanceof RouteMatch) { 100 | $helper->setRouteMatch($match); 101 | } 102 | 103 | return $helper; 104 | }; 105 | } 106 | 107 | /** 108 | * Create and return a factory for creating a BasePath helper. 109 | * 110 | * Uses configuration and request services to configure the helper. 111 | * 112 | * @param ContainerInterface $services 113 | * @return callable 114 | */ 115 | private function createBasePathHelperFactory(ContainerInterface $services) 116 | { 117 | return function () use ($services) { 118 | $config = $services->has('config') ? $services->get('config') : []; 119 | $helper = new ViewHelper\BasePath; 120 | 121 | if (isset($config['view_manager']) && isset($config['view_manager']['base_path'])) { 122 | $helper->setBasePath($config['view_manager']['base_path']); 123 | return $helper; 124 | } 125 | 126 | $request = $services->get('Request'); 127 | 128 | if (is_callable([$request, 'getBasePath'])) { 129 | $helper->setBasePath($request->getBasePath()); 130 | } 131 | 132 | return $helper; 133 | }; 134 | } 135 | 136 | /** 137 | * Create and return a Doctype helper factory. 138 | * 139 | * Other view helpers depend on this to decide which spec to generate their tags 140 | * based on. This is why it must be set early instead of later in the layout phtml. 141 | * 142 | * @param ContainerInterface $services 143 | * @return callable 144 | */ 145 | private function createDoctypeHelperFactory(ContainerInterface $services) 146 | { 147 | return function () use ($services) { 148 | $config = $services->has('config') ? $services->get('config') : []; 149 | $config = isset($config['view_manager']) ? $config['view_manager'] : []; 150 | $helper = new ViewHelper\Doctype; 151 | if (isset($config['doctype']) && $config['doctype']) { 152 | $helper->setDoctype($config['doctype']); 153 | } 154 | return $helper; 155 | }; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Controller/PluginManager.php: -------------------------------------------------------------------------------- 1 | Plugin\AcceptableViewModelSelector::class, 35 | 'acceptableViewModelSelector' => Plugin\AcceptableViewModelSelector::class, 36 | 'acceptableviewmodelselector' => Plugin\AcceptableViewModelSelector::class, 37 | 'Forward' => Plugin\Forward::class, 38 | 'forward' => Plugin\Forward::class, 39 | 'Layout' => Plugin\Layout::class, 40 | 'layout' => Plugin\Layout::class, 41 | 'Params' => Plugin\Params::class, 42 | 'params' => Plugin\Params::class, 43 | 'Redirect' => Plugin\Redirect::class, 44 | 'redirect' => Plugin\Redirect::class, 45 | 'Url' => Plugin\Url::class, 46 | 'url' => Plugin\Url::class, 47 | 'CreateHttpNotFoundModel' => Plugin\CreateHttpNotFoundModel::class, 48 | 'createHttpNotFoundModel' => Plugin\CreateHttpNotFoundModel::class, 49 | 'createhttpnotfoundmodel' => Plugin\CreateHttpNotFoundModel::class, 50 | ]; 51 | 52 | /** 53 | * @var string[]|callable[] Default factories 54 | */ 55 | protected $factories = [ 56 | Plugin\Forward::class => Plugin\Service\ForwardFactory::class, 57 | Plugin\AcceptableViewModelSelector::class => InvokableFactory::class, 58 | Plugin\Layout::class => InvokableFactory::class, 59 | Plugin\Params::class => InvokableFactory::class, 60 | Plugin\Redirect::class => InvokableFactory::class, 61 | Plugin\Url::class => InvokableFactory::class, 62 | Plugin\CreateHttpNotFoundModel::class => InvokableFactory::class, 63 | 64 | // v2 normalized names 65 | 66 | 'zendmvccontrollerpluginforward' => Plugin\Service\ForwardFactory::class, 67 | 'zendmvccontrollerpluginacceptableviewmodelselector' => InvokableFactory::class, 68 | 'zendmvccontrollerpluginlayout' => InvokableFactory::class, 69 | 'zendmvccontrollerpluginparams' => InvokableFactory::class, 70 | 'zendmvccontrollerpluginredirect' => InvokableFactory::class, 71 | 'zendmvccontrollerpluginurl' => InvokableFactory::class, 72 | 'zendmvccontrollerplugincreatehttpnotfoundmodel' => InvokableFactory::class, 73 | ]; 74 | 75 | /** 76 | * @var DispatchableInterface 77 | */ 78 | protected $controller; 79 | 80 | /** 81 | * Retrieve a registered instance 82 | * 83 | * After the plugin is retrieved from the service locator, inject the 84 | * controller in the plugin every time it is requested. This is required 85 | * because a controller can use a plugin and another controller can be 86 | * dispatched afterwards. If this second controller uses the same plugin 87 | * as the first controller, the reference to the controller inside the 88 | * plugin is lost. 89 | * 90 | * @param string $name 91 | * @param null|array $options Options to use when creating the instance. 92 | * @return DispatchableInterface 93 | */ 94 | public function get($name, array $options = null) 95 | { 96 | $plugin = parent::get($name, $options); 97 | $this->injectController($plugin); 98 | 99 | return $plugin; 100 | } 101 | 102 | /** 103 | * Set controller 104 | * 105 | * @param DispatchableInterface $controller 106 | * @return PluginManager 107 | */ 108 | public function setController(DispatchableInterface $controller) 109 | { 110 | $this->controller = $controller; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Retrieve controller instance 117 | * 118 | * @return null|DispatchableInterface 119 | */ 120 | public function getController() 121 | { 122 | return $this->controller; 123 | } 124 | 125 | /** 126 | * Inject a helper instance with the registered controller 127 | * 128 | * @param object $plugin 129 | * @return void 130 | */ 131 | public function injectController($plugin) 132 | { 133 | if (! is_object($plugin)) { 134 | return; 135 | } 136 | if (! method_exists($plugin, 'setController')) { 137 | return; 138 | } 139 | 140 | $controller = $this->getController(); 141 | if (! $controller instanceof DispatchableInterface) { 142 | return; 143 | } 144 | 145 | $plugin->setController($controller); 146 | } 147 | 148 | /** 149 | * Validate a plugin 150 | * 151 | * {@inheritDoc} 152 | */ 153 | public function validate($plugin) 154 | { 155 | if (! $plugin instanceof $this->instanceOf) { 156 | throw new InvalidServiceException(sprintf( 157 | 'Plugin of type "%s" is invalid; must implement %s', 158 | (is_object($plugin) ? get_class($plugin) : gettype($plugin)), 159 | $this->instanceOf 160 | )); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/View/Http/InjectTemplateListener.php: -------------------------------------------------------------------------------- 1 | template mappings 20 | * 21 | * @var array 22 | */ 23 | protected $controllerMap = []; 24 | 25 | /** 26 | * Flag to force the use of the route match controller param 27 | * 28 | * @var boolean 29 | */ 30 | protected $preferRouteMatchController = false; 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function attach(Events $events, $priority = 1) 36 | { 37 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'injectTemplate'], -90); 38 | } 39 | 40 | /** 41 | * Inject a template into the view model, if none present 42 | * 43 | * Template is derived from the controller found in the route match, and, 44 | * optionally, the action, if present. 45 | * 46 | * @param MvcEvent $e 47 | * @return void 48 | */ 49 | public function injectTemplate(MvcEvent $e) 50 | { 51 | $model = $e->getResult(); 52 | if (! $model instanceof ViewModel) { 53 | return; 54 | } 55 | 56 | $template = $model->getTemplate(); 57 | if (! empty($template)) { 58 | return; 59 | } 60 | 61 | $routeMatch = $e->getRouteMatch(); 62 | if ($preferRouteMatchController = $routeMatch->getParam('prefer_route_match_controller', false)) { 63 | $this->setPreferRouteMatchController($preferRouteMatchController); 64 | } 65 | 66 | $controller = $e->getTarget(); 67 | if (is_object($controller)) { 68 | $controller = get_class($controller); 69 | } 70 | 71 | $routeMatchController = $routeMatch->getParam('controller', ''); 72 | if (! $controller || ($this->preferRouteMatchController && $routeMatchController)) { 73 | $controller = $routeMatchController; 74 | } 75 | 76 | $template = $this->mapController($controller); 77 | 78 | $action = $routeMatch->getParam('action'); 79 | if (null !== $action) { 80 | $template .= '/' . $this->inflectName($action); 81 | } 82 | $model->setTemplate($template); 83 | } 84 | 85 | /** 86 | * Set map of controller namespace -> template pairs 87 | * 88 | * @param array $map 89 | * @return self 90 | */ 91 | public function setControllerMap(array $map) 92 | { 93 | krsort($map); 94 | $this->controllerMap = $map; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Maps controller to template if controller namespace is whitelisted or mapped 100 | * 101 | * @param string $controller controller FQCN 102 | * @return string|false template name or false if controller was not matched 103 | */ 104 | public function mapController($controller) 105 | { 106 | $mapped = ''; 107 | foreach ($this->controllerMap as $namespace => $replacement) { 108 | if (// Allow disabling rule by setting value to false since config 109 | // merging have no feature to remove entries 110 | false == $replacement 111 | // Match full class or full namespace 112 | || ! ($controller === $namespace || strpos($controller, $namespace . '\\') === 0) 113 | ) { 114 | continue; 115 | } 116 | 117 | // Map namespace to $replacement if its value is string 118 | if (is_string($replacement)) { 119 | $mapped = rtrim($replacement, '/') . '/'; 120 | $controller = substr($controller, strlen($namespace) + 1) ?: ''; 121 | break; 122 | } 123 | } 124 | 125 | //strip Controller namespace(s) (but not classname) 126 | $parts = explode('\\', $controller); 127 | array_pop($parts); 128 | $parts = array_diff($parts, ['Controller']); 129 | //strip trailing Controller in class name 130 | $parts[] = $this->deriveControllerClass($controller); 131 | $controller = implode('/', $parts); 132 | 133 | $template = trim($mapped . $controller, '/'); 134 | 135 | // inflect CamelCase to dash 136 | return $this->inflectName($template); 137 | } 138 | 139 | /** 140 | * Inflect a name to a normalized value 141 | * 142 | * Inlines the logic from zend-filter's Word\CamelCaseToDash filter. 143 | * 144 | * @param string $name 145 | * @return string 146 | */ 147 | protected function inflectName($name) 148 | { 149 | if (StringUtils::hasPcreUnicodeSupport()) { 150 | $pattern = ['#(?<=(?:\p{Lu}))(\p{Lu}\p{Ll})#', '#(?<=(?:\p{Ll}|\p{Nd}))(\p{Lu})#']; 151 | $replacement = ['-\1', '-\1']; 152 | } else { 153 | $pattern = ['#(?<=(?:[A-Z]))([A-Z]+)([A-Z][a-z])#', '#(?<=(?:[a-z0-9]))([A-Z])#']; 154 | $replacement = ['\1-\2', '-\1']; 155 | } 156 | 157 | $name = preg_replace($pattern, $replacement, $name); 158 | return strtolower($name); 159 | } 160 | 161 | /** 162 | * Determine the name of the controller 163 | * 164 | * Strip the namespace, and the suffix "Controller" if present. 165 | * 166 | * @param string $controller 167 | * @return string 168 | */ 169 | protected function deriveControllerClass($controller) 170 | { 171 | if (false !== strpos($controller, '\\')) { 172 | $controller = substr($controller, strrpos($controller, '\\') + 1); 173 | } 174 | 175 | if ((10 < strlen($controller)) 176 | && ('Controller' == substr($controller, -10)) 177 | ) { 178 | $controller = substr($controller, 0, -10); 179 | } 180 | 181 | return $controller; 182 | } 183 | 184 | /** 185 | * Sets the flag to instruct the listener to prefer the route match controller param 186 | * over the class name 187 | * 188 | * @param boolean $preferRouteMatchController 189 | */ 190 | public function setPreferRouteMatchController($preferRouteMatchController) 191 | { 192 | $this->preferRouteMatchController = (bool) $preferRouteMatchController; 193 | } 194 | 195 | /** 196 | * @return boolean 197 | */ 198 | public function isPreferRouteMatchController() 199 | { 200 | return $this->preferRouteMatchController; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/MiddlewareListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch'], 1); 34 | } 35 | 36 | /** 37 | * Listen to the "dispatch" event 38 | * 39 | * @param MvcEvent $event 40 | * @return mixed 41 | */ 42 | public function onDispatch(MvcEvent $event) 43 | { 44 | if (null !== $event->getResult()) { 45 | return; 46 | } 47 | 48 | $routeMatch = $event->getRouteMatch(); 49 | $middleware = $routeMatch->getParam('middleware', false); 50 | if (false === $middleware) { 51 | return; 52 | } 53 | 54 | $request = $event->getRequest(); 55 | $application = $event->getApplication(); 56 | $response = $application->getResponse(); 57 | $serviceManager = $application->getServiceManager(); 58 | 59 | $psr7ResponsePrototype = Psr7Response::fromZend($response); 60 | 61 | try { 62 | $pipe = $this->createPipeFromSpec( 63 | $serviceManager, 64 | $psr7ResponsePrototype, 65 | is_array($middleware) ? $middleware : [$middleware] 66 | ); 67 | } catch (InvalidMiddlewareException $invalidMiddlewareException) { 68 | $return = $this->marshalInvalidMiddleware( 69 | $application::ERROR_MIDDLEWARE_CANNOT_DISPATCH, 70 | $invalidMiddlewareException->toMiddlewareName(), 71 | $event, 72 | $application, 73 | $invalidMiddlewareException 74 | ); 75 | $event->setResult($return); 76 | return $return; 77 | } 78 | 79 | $caughtException = null; 80 | try { 81 | $return = (new MiddlewareController( 82 | $pipe, 83 | $psr7ResponsePrototype, 84 | $application->getServiceManager()->get('EventManager'), 85 | $event 86 | ))->dispatch($request, $response); 87 | } catch (\Throwable $ex) { 88 | $caughtException = $ex; 89 | } catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced 90 | $caughtException = $ex; 91 | } 92 | 93 | if ($caughtException !== null) { 94 | $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); 95 | $event->setError($application::ERROR_EXCEPTION); 96 | $event->setParam('exception', $caughtException); 97 | 98 | $events = $application->getEventManager(); 99 | $results = $events->triggerEvent($event); 100 | $return = $results->last(); 101 | if (! $return) { 102 | $return = $event->getResult(); 103 | } 104 | } 105 | 106 | $event->setError(''); 107 | 108 | if (! $return instanceof PsrResponseInterface) { 109 | $event->setResult($return); 110 | return $return; 111 | } 112 | $response = Psr7Response::toZend($return); 113 | $event->setResult($response); 114 | return $response; 115 | } 116 | 117 | /** 118 | * Create a middleware pipe from the array spec given. 119 | * 120 | * @param ContainerInterface $serviceLocator 121 | * @param ResponseInterface $responsePrototype 122 | * @param array $middlewaresToBePiped 123 | * @return MiddlewarePipe 124 | * @throws InvalidMiddlewareException 125 | */ 126 | private function createPipeFromSpec( 127 | ContainerInterface $serviceLocator, 128 | ResponseInterface $responsePrototype, 129 | array $middlewaresToBePiped 130 | ) { 131 | $pipe = new MiddlewarePipe(); 132 | $pipe->setResponsePrototype($responsePrototype); 133 | foreach ($middlewaresToBePiped as $middlewareToBePiped) { 134 | if (null === $middlewareToBePiped) { 135 | throw InvalidMiddlewareException::fromNull(); 136 | } 137 | 138 | $middlewareName = is_string($middlewareToBePiped) ? $middlewareToBePiped : get_class($middlewareToBePiped); 139 | 140 | if (is_string($middlewareToBePiped) && $serviceLocator->has($middlewareToBePiped)) { 141 | $middlewareToBePiped = $serviceLocator->get($middlewareToBePiped); 142 | } 143 | if (! $middlewareToBePiped instanceof MiddlewareInterface && ! is_callable($middlewareToBePiped)) { 144 | throw InvalidMiddlewareException::fromMiddlewareName($middlewareName); 145 | } 146 | 147 | $pipe->pipe($middlewareToBePiped); 148 | } 149 | return $pipe; 150 | } 151 | 152 | /** 153 | * Marshal a middleware not callable exception event 154 | * 155 | * @param string $type 156 | * @param string $middlewareName 157 | * @param MvcEvent $event 158 | * @param Application $application 159 | * @param \Exception $exception 160 | * @return mixed 161 | */ 162 | protected function marshalInvalidMiddleware( 163 | $type, 164 | $middlewareName, 165 | MvcEvent $event, 166 | Application $application, 167 | \Exception $exception = null 168 | ) { 169 | $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); 170 | $event->setError($type); 171 | $event->setController($middlewareName); 172 | $event->setControllerClass('Middleware not callable: ' . $middlewareName); 173 | if ($exception !== null) { 174 | $event->setParam('exception', $exception); 175 | } 176 | 177 | $events = $application->getEventManager(); 178 | $results = $events->triggerEvent($event); 179 | $return = $results->last(); 180 | if (! $return) { 181 | $return = $event->getResult(); 182 | } 183 | return $return; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Controller/LazyControllerAbstractFactory.php: -------------------------------------------------------------------------------- 1 | 38 | * 'controllers' => [ 39 | * 'abstract_factories' => [ 40 | * LazyControllerAbstractFactory::class, 41 | * ], 42 | * ], 43 | * 44 | * 45 | * Or as a factory, mapping a controller class name to it: 46 | * 47 | * 48 | * 'controllers' => [ 49 | * 'factories' => [ 50 | * MyControllerWithDependencies::class => LazyControllerAbstractFactory::class, 51 | * ], 52 | * ], 53 | * 54 | * 55 | * The latter approach is more explicit, and also more performant. 56 | * 57 | * The factory has the following constraints/features: 58 | * 59 | * - A parameter named `$config` typehinted as an array will receive the 60 | * application "config" service (i.e., the merged configuration). 61 | * - Parameters type-hinted against array, but not named `$config` will 62 | * be injected with an empty array. 63 | * - Scalar parameters will be resolved as null values. 64 | * - If a service cannot be found for a given typehint, the factory will 65 | * raise an exception detailing this. 66 | * - Some services provided by Zend Framework components do not have 67 | * entries based on their class name (for historical reasons); the 68 | * factory contains a map of these class/interface names to the 69 | * corresponding service name to allow them to resolve. 70 | * 71 | * `$options` passed to the factory are ignored in all cases, as we cannot 72 | * make assumptions about which argument(s) they might replace. 73 | */ 74 | class LazyControllerAbstractFactory implements AbstractFactoryInterface 75 | { 76 | /** 77 | * Maps known classes/interfaces to the service that provides them; only 78 | * required for those services with no entry based on the class/interface 79 | * name. 80 | * 81 | * Extend the class if you wish to add to the list. 82 | * 83 | * @var string[] 84 | */ 85 | protected $aliases = [ 86 | ConsoleAdapterInterface::class => 'ConsoleAdapter', 87 | FilterPluginManager::class => 'FilterManager', 88 | HydratorPluginManager::class => 'HydratorManager', 89 | InputFilterPluginManager::class => 'InputFilterManager', 90 | LogFilterManager::class => 'LogFilterManager', 91 | LogFormatterManager::class => 'LogFormatterManager', 92 | LogProcessorManager::class => 'LogProcessorManager', 93 | LogWriterManager::class => 'LogWriterManager', 94 | SerializerAdapterManager::class => 'SerializerAdapterManager', 95 | ValidatorPluginManager::class => 'ValidatorManager', 96 | ]; 97 | 98 | /** 99 | * {@inheritDoc} 100 | * 101 | * @return DispatchableInterface 102 | */ 103 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 104 | { 105 | $reflectionClass = new ReflectionClass($requestedName); 106 | 107 | if (null === ($constructor = $reflectionClass->getConstructor())) { 108 | return new $requestedName(); 109 | } 110 | 111 | $reflectionParameters = $constructor->getParameters(); 112 | 113 | if (empty($reflectionParameters)) { 114 | return new $requestedName(); 115 | } 116 | 117 | $parameters = array_map( 118 | $this->resolveParameter($container, $requestedName), 119 | $reflectionParameters 120 | ); 121 | 122 | return new $requestedName(...$parameters); 123 | } 124 | 125 | /** 126 | * {@inheritDoc} 127 | */ 128 | public function canCreate(ContainerInterface $container, $requestedName) 129 | { 130 | if (! class_exists($requestedName)) { 131 | return false; 132 | } 133 | 134 | return in_array(DispatchableInterface::class, class_implements($requestedName), true); 135 | } 136 | 137 | /** 138 | * Resolve a parameter to a value. 139 | * 140 | * Returns a callback for resolving a parameter to a value. 141 | * 142 | * @param ContainerInterface $container 143 | * @param string $requestedName 144 | * @return callable 145 | */ 146 | private function resolveParameter(ContainerInterface $container, $requestedName) 147 | { 148 | /** 149 | * @param ReflectionParameter $parameter 150 | * @return mixed 151 | * @throws ServiceNotFoundException If type-hinted parameter cannot be 152 | * resolved to a service in the container. 153 | */ 154 | return function (ReflectionParameter $parameter) use ($container, $requestedName) { 155 | if ($parameter->isArray() 156 | && $parameter->getName() === 'config' 157 | && $container->has('config') 158 | ) { 159 | return $container->get('config'); 160 | } 161 | 162 | if ($parameter->isArray()) { 163 | return []; 164 | } 165 | 166 | if (! $parameter->getClass()) { 167 | return; 168 | } 169 | 170 | $type = $parameter->getClass()->getName(); 171 | $type = isset($this->aliases[$type]) ? $this->aliases[$type] : $type; 172 | 173 | if (! $container->has($type)) { 174 | throw new ServiceNotFoundException(sprintf( 175 | 'Unable to create controller "%s"; unable to resolve parameter "%s" using type hint "%s"', 176 | $requestedName, 177 | $parameter->getName(), 178 | $type 179 | )); 180 | } 181 | 182 | return $container->get($type); 183 | }; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/MvcEvent.php: -------------------------------------------------------------------------------- 1 | setParam('application', $application); 73 | $this->application = $application; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Get application instance 79 | * 80 | * @return ApplicationInterface 81 | */ 82 | public function getApplication() 83 | { 84 | return $this->application; 85 | } 86 | 87 | /** 88 | * Get router 89 | * 90 | * @return RouteStackInterface 91 | */ 92 | public function getRouter() 93 | { 94 | return $this->router; 95 | } 96 | 97 | /** 98 | * Set router 99 | * 100 | * @param RouteStackInterface $router 101 | * @return MvcEvent 102 | */ 103 | public function setRouter(RouteStackInterface $router) 104 | { 105 | $this->setParam('router', $router); 106 | $this->router = $router; 107 | return $this; 108 | } 109 | 110 | /** 111 | * Get route match 112 | * 113 | * @return null|RouteMatch 114 | */ 115 | public function getRouteMatch() 116 | { 117 | return $this->routeMatch; 118 | } 119 | 120 | /** 121 | * Set route match 122 | * 123 | * @param RouteMatch $matches 124 | * @return MvcEvent 125 | */ 126 | public function setRouteMatch(RouteMatch $matches) 127 | { 128 | $this->setParam('route-match', $matches); 129 | $this->routeMatch = $matches; 130 | return $this; 131 | } 132 | 133 | /** 134 | * Get request 135 | * 136 | * @return Request 137 | */ 138 | public function getRequest() 139 | { 140 | return $this->request; 141 | } 142 | 143 | /** 144 | * Set request 145 | * 146 | * @param Request $request 147 | * @return MvcEvent 148 | */ 149 | public function setRequest(Request $request) 150 | { 151 | $this->setParam('request', $request); 152 | $this->request = $request; 153 | return $this; 154 | } 155 | 156 | /** 157 | * Get response 158 | * 159 | * @return Response 160 | */ 161 | public function getResponse() 162 | { 163 | return $this->response; 164 | } 165 | 166 | /** 167 | * Set response 168 | * 169 | * @param Response $response 170 | * @return MvcEvent 171 | */ 172 | public function setResponse(Response $response) 173 | { 174 | $this->setParam('response', $response); 175 | $this->response = $response; 176 | return $this; 177 | } 178 | 179 | /** 180 | * Set the view model 181 | * 182 | * @param Model $viewModel 183 | * @return MvcEvent 184 | */ 185 | public function setViewModel(Model $viewModel) 186 | { 187 | $this->viewModel = $viewModel; 188 | return $this; 189 | } 190 | 191 | /** 192 | * Get the view model 193 | * 194 | * @return Model 195 | */ 196 | public function getViewModel() 197 | { 198 | if (null === $this->viewModel) { 199 | $this->setViewModel(new ViewModel()); 200 | } 201 | return $this->viewModel; 202 | } 203 | 204 | /** 205 | * Get result 206 | * 207 | * @return mixed 208 | */ 209 | public function getResult() 210 | { 211 | return $this->result; 212 | } 213 | 214 | /** 215 | * Set result 216 | * 217 | * @param mixed $result 218 | * @return MvcEvent 219 | */ 220 | public function setResult($result) 221 | { 222 | $this->setParam('__RESULT__', $result); 223 | $this->result = $result; 224 | return $this; 225 | } 226 | 227 | /** 228 | * Does the event represent an error response? 229 | * 230 | * @return bool 231 | */ 232 | public function isError() 233 | { 234 | return (bool) $this->getParam('error', false); 235 | } 236 | 237 | /** 238 | * Set the error message (indicating error in handling request) 239 | * 240 | * @param string $message 241 | * @return MvcEvent 242 | */ 243 | public function setError($message) 244 | { 245 | $this->setParam('error', $message); 246 | return $this; 247 | } 248 | 249 | /** 250 | * Retrieve the error message, if any 251 | * 252 | * @return string 253 | */ 254 | public function getError() 255 | { 256 | return $this->getParam('error', ''); 257 | } 258 | 259 | /** 260 | * Get the currently registered controller name 261 | * 262 | * @return string 263 | */ 264 | public function getController() 265 | { 266 | return $this->getParam('controller'); 267 | } 268 | 269 | /** 270 | * Set controller name 271 | * 272 | * @param string $name 273 | * @return MvcEvent 274 | */ 275 | public function setController($name) 276 | { 277 | $this->setParam('controller', $name); 278 | return $this; 279 | } 280 | 281 | /** 282 | * Get controller class 283 | * 284 | * @return string 285 | */ 286 | public function getControllerClass() 287 | { 288 | return $this->getParam('controller-class'); 289 | } 290 | 291 | /** 292 | * Set controller class 293 | * 294 | * @param string $class 295 | * @return MvcEvent 296 | */ 297 | public function setControllerClass($class) 298 | { 299 | $this->setParam('controller-class', $class); 300 | return $this; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | request = $request; 91 | if (! $response) { 92 | $response = new HttpResponse(); 93 | } 94 | $this->response = $response; 95 | 96 | $e = $this->getEvent(); 97 | $e->setName(MvcEvent::EVENT_DISPATCH); 98 | $e->setRequest($request); 99 | $e->setResponse($response); 100 | $e->setTarget($this); 101 | 102 | $result = $this->getEventManager()->triggerEventUntil(function ($test) { 103 | return ($test instanceof Response); 104 | }, $e); 105 | 106 | if ($result->stopped()) { 107 | return $result->last(); 108 | } 109 | 110 | return $e->getResult(); 111 | } 112 | 113 | /** 114 | * Get request object 115 | * 116 | * @return Request 117 | */ 118 | public function getRequest() 119 | { 120 | if (! $this->request) { 121 | $this->request = new HttpRequest(); 122 | } 123 | 124 | return $this->request; 125 | } 126 | 127 | /** 128 | * Get response object 129 | * 130 | * @return Response 131 | */ 132 | public function getResponse() 133 | { 134 | if (! $this->response) { 135 | $this->response = new HttpResponse(); 136 | } 137 | 138 | return $this->response; 139 | } 140 | 141 | /** 142 | * Set the event manager instance used by this context 143 | * 144 | * @param EventManagerInterface $events 145 | * @return AbstractController 146 | */ 147 | public function setEventManager(EventManagerInterface $events) 148 | { 149 | $className = get_class($this); 150 | 151 | $nsPos = strpos($className, '\\') ?: 0; 152 | $events->setIdentifiers(array_merge( 153 | [ 154 | __CLASS__, 155 | $className, 156 | substr($className, 0, $nsPos) 157 | ], 158 | array_values(class_implements($className)), 159 | (array) $this->eventIdentifier 160 | )); 161 | 162 | $this->events = $events; 163 | $this->attachDefaultListeners(); 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * Retrieve the event manager 170 | * 171 | * Lazy-loads an EventManager instance if none registered. 172 | * 173 | * @return EventManagerInterface 174 | */ 175 | public function getEventManager() 176 | { 177 | if (! $this->events) { 178 | $this->setEventManager(new EventManager()); 179 | } 180 | 181 | return $this->events; 182 | } 183 | 184 | /** 185 | * Set an event to use during dispatch 186 | * 187 | * By default, will re-cast to MvcEvent if another event type is provided. 188 | * 189 | * @param Event $e 190 | * @return void 191 | */ 192 | public function setEvent(Event $e) 193 | { 194 | if (! $e instanceof MvcEvent) { 195 | $eventParams = $e->getParams(); 196 | $e = new MvcEvent(); 197 | $e->setParams($eventParams); 198 | unset($eventParams); 199 | } 200 | $this->event = $e; 201 | } 202 | 203 | /** 204 | * Get the attached event 205 | * 206 | * Will create a new MvcEvent if none provided. 207 | * 208 | * @return MvcEvent 209 | */ 210 | public function getEvent() 211 | { 212 | if (! $this->event) { 213 | $this->setEvent(new MvcEvent()); 214 | } 215 | 216 | return $this->event; 217 | } 218 | 219 | /** 220 | * Get plugin manager 221 | * 222 | * @return PluginManager 223 | */ 224 | public function getPluginManager() 225 | { 226 | if (! $this->plugins) { 227 | $this->setPluginManager(new PluginManager(new ServiceManager())); 228 | } 229 | 230 | $this->plugins->setController($this); 231 | return $this->plugins; 232 | } 233 | 234 | /** 235 | * Set plugin manager 236 | * 237 | * @param PluginManager $plugins 238 | * @return AbstractController 239 | */ 240 | public function setPluginManager(PluginManager $plugins) 241 | { 242 | $this->plugins = $plugins; 243 | $this->plugins->setController($this); 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Get plugin instance 250 | * 251 | * @param string $name Name of plugin to return 252 | * @param null|array $options Options to pass to plugin constructor (if not already instantiated) 253 | * @return mixed 254 | */ 255 | public function plugin($name, array $options = null) 256 | { 257 | return $this->getPluginManager()->get($name, $options); 258 | } 259 | 260 | /** 261 | * Method overloading: return/call plugins 262 | * 263 | * If the plugin is a functor, call it, passing the parameters provided. 264 | * Otherwise, return the plugin instance. 265 | * 266 | * @param string $method 267 | * @param array $params 268 | * @return mixed 269 | */ 270 | public function __call($method, $params) 271 | { 272 | $plugin = $this->plugin($method); 273 | if (is_callable($plugin)) { 274 | return call_user_func_array($plugin, $params); 275 | } 276 | 277 | return $plugin; 278 | } 279 | 280 | /** 281 | * Register the default events for this controller 282 | * 283 | * @return void 284 | */ 285 | protected function attachDefaultListeners() 286 | { 287 | $events = $this->getEventManager(); 288 | $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']); 289 | } 290 | 291 | /** 292 | * Transform an "action" token into a method name 293 | * 294 | * @param string $action 295 | * @return string 296 | */ 297 | public static function getMethodFromAction($action) 298 | { 299 | $method = str_replace(['.', '-', '_'], ' ', $action); 300 | $method = ucwords($method); 301 | $method = str_replace(' ', '', $method); 302 | $method = lcfirst($method); 303 | $method .= 'Action'; 304 | 305 | return $method; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/DispatchListener.php: -------------------------------------------------------------------------------- 1 | controllerManager = $controllerManager; 52 | } 53 | 54 | /** 55 | * Attach listeners to an event manager 56 | * 57 | * @param EventManagerInterface $events 58 | * @param int $priority 59 | * @return void 60 | */ 61 | public function attach(EventManagerInterface $events, $priority = 1) 62 | { 63 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']); 64 | if (function_exists('zend_monitor_custom_event_ex')) { 65 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'reportMonitorEvent']); 66 | } 67 | } 68 | 69 | /** 70 | * Listen to the "dispatch" event 71 | * 72 | * @param MvcEvent $e 73 | * @return mixed 74 | */ 75 | public function onDispatch(MvcEvent $e) 76 | { 77 | if (null !== $e->getResult()) { 78 | return; 79 | } 80 | 81 | $routeMatch = $e->getRouteMatch(); 82 | $controllerName = $routeMatch instanceof RouteMatch 83 | ? $routeMatch->getParam('controller', 'not-found') 84 | : 'not-found'; 85 | $application = $e->getApplication(); 86 | $controllerManager = $this->controllerManager; 87 | 88 | 89 | // Query abstract controllers, too! 90 | if (! $controllerManager->has($controllerName)) { 91 | $return = $this->marshalControllerNotFoundEvent( 92 | $application::ERROR_CONTROLLER_NOT_FOUND, 93 | $controllerName, 94 | $e, 95 | $application 96 | ); 97 | return $this->complete($return, $e); 98 | } 99 | 100 | try { 101 | $controller = $controllerManager->get($controllerName); 102 | } catch (Exception\InvalidControllerException $exception) { 103 | $return = $this->marshalControllerNotFoundEvent( 104 | $application::ERROR_CONTROLLER_INVALID, 105 | $controllerName, 106 | $e, 107 | $application, 108 | $exception 109 | ); 110 | return $this->complete($return, $e); 111 | } catch (InvalidServiceException $exception) { 112 | $return = $this->marshalControllerNotFoundEvent( 113 | $application::ERROR_CONTROLLER_INVALID, 114 | $controllerName, 115 | $e, 116 | $application, 117 | $exception 118 | ); 119 | return $this->complete($return, $e); 120 | } catch (\Throwable $exception) { 121 | $return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception); 122 | return $this->complete($return, $e); 123 | } catch (\Exception $exception) { // @TODO clean up once PHP 7 requirement is enforced 124 | $return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception); 125 | return $this->complete($return, $e); 126 | } 127 | 128 | if ($controller instanceof InjectApplicationEventInterface) { 129 | $controller->setEvent($e); 130 | } 131 | 132 | $request = $e->getRequest(); 133 | $response = $application->getResponse(); 134 | $caughtException = null; 135 | 136 | try { 137 | $return = $controller->dispatch($request, $response); 138 | } catch (\Throwable $ex) { 139 | $caughtException = $ex; 140 | } catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced 141 | $caughtException = $ex; 142 | } 143 | 144 | if ($caughtException !== null) { 145 | $e->setName(MvcEvent::EVENT_DISPATCH_ERROR); 146 | $e->setError($application::ERROR_EXCEPTION); 147 | $e->setController($controllerName); 148 | $e->setControllerClass(get_class($controller)); 149 | $e->setParam('exception', $caughtException); 150 | 151 | $return = $application->getEventManager()->triggerEvent($e)->last(); 152 | if (! $return) { 153 | $return = $e->getResult(); 154 | } 155 | } 156 | 157 | return $this->complete($return, $e); 158 | } 159 | 160 | /** 161 | * @param MvcEvent $e 162 | */ 163 | public function reportMonitorEvent(MvcEvent $e) 164 | { 165 | $error = $e->getError(); 166 | $exception = $e->getParam('exception'); 167 | // @TODO clean up once PHP 7 requirement is enforced 168 | if ($exception instanceof \Exception || $exception instanceof \Throwable) { 169 | zend_monitor_custom_event_ex( 170 | $error, 171 | $exception->getMessage(), 172 | 'Zend Framework Exception', 173 | ['code' => $exception->getCode(), 'trace' => $exception->getTraceAsString()] 174 | ); 175 | } 176 | } 177 | 178 | /** 179 | * Complete the dispatch 180 | * 181 | * @param mixed $return 182 | * @param MvcEvent $event 183 | * @return mixed 184 | */ 185 | protected function complete($return, MvcEvent $event) 186 | { 187 | if (! is_object($return)) { 188 | if (ArrayUtils::hasStringKeys($return)) { 189 | $return = new ArrayObject($return, ArrayObject::ARRAY_AS_PROPS); 190 | } 191 | } 192 | $event->setResult($return); 193 | return $return; 194 | } 195 | 196 | /** 197 | * Marshal a controller not found exception event 198 | * 199 | * @param string $type 200 | * @param string $controllerName 201 | * @param MvcEvent $event 202 | * @param Application $application 203 | * @param \Throwable|\Exception $exception 204 | * @return mixed 205 | */ 206 | protected function marshalControllerNotFoundEvent( 207 | $type, 208 | $controllerName, 209 | MvcEvent $event, 210 | Application $application, 211 | $exception = null 212 | ) { 213 | $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); 214 | $event->setError($type); 215 | $event->setController($controllerName); 216 | $event->setControllerClass('invalid controller class or alias: ' . $controllerName); 217 | if ($exception !== null) { 218 | $event->setParam('exception', $exception); 219 | } 220 | 221 | $events = $application->getEventManager(); 222 | $results = $events->triggerEvent($event); 223 | $return = $results->last(); 224 | if (! $return) { 225 | $return = $event->getResult(); 226 | } 227 | return $return; 228 | } 229 | 230 | /** 231 | * Marshal a bad controller exception event 232 | * 233 | * @param string $controllerName 234 | * @param MvcEvent $event 235 | * @param Application $application 236 | * @param \Throwable|\Exception $exception 237 | * @return mixed 238 | */ 239 | protected function marshalBadControllerEvent( 240 | $controllerName, 241 | MvcEvent $event, 242 | Application $application, 243 | $exception 244 | ) { 245 | $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); 246 | $event->setError($application::ERROR_EXCEPTION); 247 | $event->setController($controllerName); 248 | $event->setParam('exception', $exception); 249 | 250 | $events = $application->getEventManager(); 251 | $results = $events->triggerEvent($event); 252 | $return = $results->last(); 253 | if (! $return) { 254 | return $event->getResult(); 255 | } 256 | 257 | return $return; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/View/Http/RouteNotFoundStrategy.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, [$this, 'prepareNotFoundViewModel'], -90); 54 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'detectNotFoundError']); 55 | $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'prepareNotFoundViewModel']); 56 | } 57 | 58 | /** 59 | * Set value indicating whether or not to display exceptions related to a not-found condition 60 | * 61 | * @param bool $displayExceptions 62 | * @return RouteNotFoundStrategy 63 | */ 64 | public function setDisplayExceptions($displayExceptions) 65 | { 66 | $this->displayExceptions = (bool) $displayExceptions; 67 | return $this; 68 | } 69 | 70 | /** 71 | * Should we display exceptions related to a not-found condition? 72 | * 73 | * @return bool 74 | */ 75 | public function displayExceptions() 76 | { 77 | return $this->displayExceptions; 78 | } 79 | 80 | /** 81 | * Set value indicating whether or not to display the reason for a not-found condition 82 | * 83 | * @param bool $displayNotFoundReason 84 | * @return RouteNotFoundStrategy 85 | */ 86 | public function setDisplayNotFoundReason($displayNotFoundReason) 87 | { 88 | $this->displayNotFoundReason = (bool) $displayNotFoundReason; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Should we display the reason for a not-found condition? 94 | * 95 | * @return bool 96 | */ 97 | public function displayNotFoundReason() 98 | { 99 | return $this->displayNotFoundReason; 100 | } 101 | 102 | /** 103 | * Get template for not found conditions 104 | * 105 | * @param string $notFoundTemplate 106 | * @return RouteNotFoundStrategy 107 | */ 108 | public function setNotFoundTemplate($notFoundTemplate) 109 | { 110 | $this->notFoundTemplate = (string) $notFoundTemplate; 111 | return $this; 112 | } 113 | 114 | /** 115 | * Get template for not found conditions 116 | * 117 | * @return string 118 | */ 119 | public function getNotFoundTemplate() 120 | { 121 | return $this->notFoundTemplate; 122 | } 123 | 124 | /** 125 | * Detect if an error is a 404 condition 126 | * 127 | * If a "controller not found" or "invalid controller" error type is 128 | * encountered, sets the response status code to 404. 129 | * 130 | * @param MvcEvent $e 131 | * @return void 132 | */ 133 | public function detectNotFoundError(MvcEvent $e) 134 | { 135 | $error = $e->getError(); 136 | if (empty($error)) { 137 | return; 138 | } 139 | 140 | switch ($error) { 141 | case Application::ERROR_CONTROLLER_NOT_FOUND: 142 | case Application::ERROR_CONTROLLER_INVALID: 143 | case Application::ERROR_ROUTER_NO_MATCH: 144 | $this->reason = $error; 145 | $response = $e->getResponse(); 146 | if (! $response) { 147 | $response = new HttpResponse(); 148 | $e->setResponse($response); 149 | } 150 | $response->setStatusCode(404); 151 | break; 152 | default: 153 | return; 154 | } 155 | } 156 | 157 | /** 158 | * Create and return a 404 view model 159 | * 160 | * @param MvcEvent $e 161 | * @return void 162 | */ 163 | public function prepareNotFoundViewModel(MvcEvent $e) 164 | { 165 | $vars = $e->getResult(); 166 | if ($vars instanceof Response) { 167 | // Already have a response as the result 168 | return; 169 | } 170 | 171 | $response = $e->getResponse(); 172 | if ($response->getStatusCode() != 404) { 173 | // Only handle 404 responses 174 | return; 175 | } 176 | 177 | if (! $vars instanceof ViewModel) { 178 | $model = new ViewModel(); 179 | if (is_string($vars)) { 180 | $model->setVariable('message', $vars); 181 | } else { 182 | $model->setVariable('message', 'Page not found.'); 183 | } 184 | } else { 185 | $model = $vars; 186 | if ($model->getVariable('message') === null) { 187 | $model->setVariable('message', 'Page not found.'); 188 | } 189 | } 190 | 191 | $model->setTemplate($this->getNotFoundTemplate()); 192 | 193 | // If displaying reasons, inject the reason 194 | $this->injectNotFoundReason($model); 195 | 196 | // If displaying exceptions, inject 197 | $this->injectException($model, $e); 198 | 199 | // Inject controller if we're displaying either the reason or the exception 200 | $this->injectController($model, $e); 201 | 202 | $e->setResult($model); 203 | } 204 | 205 | /** 206 | * Inject the not-found reason into the model 207 | * 208 | * If $displayNotFoundReason is enabled, checks to see if $reason is set, 209 | * and, if so, injects it into the model. If not, it injects 210 | * Application::ERROR_CONTROLLER_CANNOT_DISPATCH. 211 | * 212 | * @param ViewModel $model 213 | * @return void 214 | */ 215 | protected function injectNotFoundReason(ViewModel $model) 216 | { 217 | if (! $this->displayNotFoundReason()) { 218 | return; 219 | } 220 | 221 | // no route match, controller not found, or controller invalid 222 | if ($this->reason) { 223 | $model->setVariable('reason', $this->reason); 224 | return; 225 | } 226 | 227 | // otherwise, must be a case of the controller not being able to 228 | // dispatch itself. 229 | $model->setVariable('reason', Application::ERROR_CONTROLLER_CANNOT_DISPATCH); 230 | } 231 | 232 | /** 233 | * Inject the exception message into the model 234 | * 235 | * If $displayExceptions is enabled, and an exception is found in the 236 | * event, inject it into the model. 237 | * 238 | * @param ViewModel $model 239 | * @param MvcEvent $e 240 | * @return void 241 | */ 242 | protected function injectException($model, $e) 243 | { 244 | if (! $this->displayExceptions()) { 245 | return; 246 | } 247 | 248 | $model->setVariable('display_exceptions', true); 249 | 250 | $exception = $e->getParam('exception', false); 251 | 252 | // @TODO clean up once PHP 7 requirement is enforced 253 | if (! $exception instanceof \Exception && ! $exception instanceof \Throwable) { 254 | return; 255 | } 256 | 257 | $model->setVariable('exception', $exception); 258 | } 259 | 260 | /** 261 | * Inject the controller and controller class into the model 262 | * 263 | * If either $displayExceptions or $displayNotFoundReason are enabled, 264 | * injects the controllerClass from the MvcEvent. It checks to see if a 265 | * controller is present in the MvcEvent, and, if not, grabs it from 266 | * the route match if present; if a controller is found, it injects it into 267 | * the model. 268 | * 269 | * @param ViewModel $model 270 | * @param MvcEvent $e 271 | * @return void 272 | */ 273 | protected function injectController($model, $e) 274 | { 275 | if (! $this->displayExceptions() && ! $this->displayNotFoundReason()) { 276 | return; 277 | } 278 | 279 | $controller = $e->getController(); 280 | if (empty($controller)) { 281 | $routeMatch = $e->getRouteMatch(); 282 | if (empty($routeMatch)) { 283 | return; 284 | } 285 | 286 | $controller = $routeMatch->getParam('controller', false); 287 | if (! $controller) { 288 | return; 289 | } 290 | } 291 | 292 | $controllerClass = $e->getControllerClass(); 293 | $model->setVariable('controller', $controller); 294 | $model->setVariable('controller_class', $controllerClass); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/Controller/Plugin/AcceptableViewModelSelector.php: -------------------------------------------------------------------------------- 1 | getViewModel($matchAgainst, $returnDefault, $resultReference); 70 | } 71 | 72 | /** 73 | * Detects an appropriate viewmodel for request. 74 | * 75 | * @param array $matchAgainst (optional) The Array to match against 76 | * @param bool $returnDefault (optional) If no match is available. Return default instead 77 | * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched 78 | * @throws InvalidArgumentException If the supplied and matched View Model could not be found 79 | * @return ModelInterface|null 80 | */ 81 | public function getViewModel( 82 | array $matchAgainst = null, 83 | $returnDefault = true, 84 | & $resultReference = null 85 | ) { 86 | $name = $this->getViewModelName($matchAgainst, $returnDefault, $resultReference); 87 | 88 | if (! $name) { 89 | return; 90 | } 91 | 92 | if (! class_exists($name)) { 93 | throw new InvalidArgumentException('The supplied View Model could not be found'); 94 | } 95 | 96 | return new $name(); 97 | } 98 | 99 | /** 100 | * Detects an appropriate viewmodel name for request. 101 | * 102 | * @param array $matchAgainst (optional) The Array to match against 103 | * @param bool $returnDefault (optional) If no match is available. Return default instead 104 | * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched. 105 | * @return ModelInterface|null Returns null if $returnDefault = false and no match could be made 106 | */ 107 | public function getViewModelName( 108 | array $matchAgainst = null, 109 | $returnDefault = true, 110 | & $resultReference = null 111 | ) { 112 | $res = $this->match($matchAgainst); 113 | if ($res) { 114 | $resultReference = $res; 115 | return $this->extractViewModelName($res); 116 | } 117 | 118 | if ($returnDefault) { 119 | return $this->defaultViewModelName; 120 | } 121 | } 122 | 123 | /** 124 | * Detects an appropriate viewmodel name for request. 125 | * 126 | * @param array $matchAgainst (optional) The Array to match against 127 | * @return AbstractFieldValuePart|null The object that was matched 128 | */ 129 | public function match(array $matchAgainst = null) 130 | { 131 | $request = $this->getRequest(); 132 | $headers = $request->getHeaders(); 133 | 134 | if ((! $matchAgainst && ! $this->defaultMatchAgainst) || ! $headers->has('accept')) { 135 | return; 136 | } 137 | 138 | if (! $matchAgainst) { 139 | $matchAgainst = $this->defaultMatchAgainst; 140 | } 141 | 142 | $matchAgainstString = ''; 143 | foreach ($matchAgainst as $modelName => $modelStrings) { 144 | foreach ((array) $modelStrings as $modelString) { 145 | $matchAgainstString .= $this->injectViewModelName($modelString, $modelName); 146 | } 147 | } 148 | 149 | /** @var $accept \Zend\Http\Header\Accept */ 150 | $accept = $headers->get('Accept'); 151 | if (($res = $accept->match($matchAgainstString)) === false) { 152 | return; 153 | } 154 | 155 | return $res; 156 | } 157 | 158 | /** 159 | * Set the default View Model (name) to return if no match could be made 160 | * @param string $defaultViewModelName The default View Model name 161 | * @return AcceptableViewModelSelector provides fluent interface 162 | */ 163 | public function setDefaultViewModelName($defaultViewModelName) 164 | { 165 | $this->defaultViewModelName = (string) $defaultViewModelName; 166 | return $this; 167 | } 168 | 169 | /** 170 | * Set the default View Model (name) to return if no match could be made 171 | * @return string 172 | */ 173 | public function getDefaultViewModelName() 174 | { 175 | return $this->defaultViewModelName; 176 | } 177 | 178 | /** 179 | * Set the default Accept Types and View Model combinations to match against if none are specified. 180 | * 181 | * @param array $matchAgainst (optional) The Array to match against 182 | * @return AcceptableViewModelSelector provides fluent interface 183 | */ 184 | public function setDefaultMatchAgainst(array $matchAgainst = null) 185 | { 186 | $this->defaultMatchAgainst = $matchAgainst; 187 | return $this; 188 | } 189 | 190 | /** 191 | * Get the default Accept Types and View Model combinations to match against if none are specified. 192 | * 193 | * @return array|null 194 | */ 195 | public function getDefaultMatchAgainst() 196 | { 197 | return $this->defaultMatchAgainst; 198 | } 199 | 200 | /** 201 | * Inject the viewmodel name into the accept header string 202 | * 203 | * @param string $modelAcceptString 204 | * @param string $modelName 205 | * @return string 206 | */ 207 | protected function injectViewModelName($modelAcceptString, $modelName) 208 | { 209 | $modelName = str_replace('\\', '|', $modelName); 210 | $modelAcceptString = (is_array($modelAcceptString)) 211 | ? $modelAcceptString[key($modelAcceptString)] 212 | : $modelAcceptString; 213 | return $modelAcceptString . '; ' . self::INJECT_VIEWMODEL_NAME . '="' . $modelName . '", '; 214 | } 215 | 216 | /** 217 | * Extract the viewmodel name from a match 218 | * @param AbstractFieldValuePart $res 219 | * @return string 220 | */ 221 | protected function extractViewModelName(AbstractFieldValuePart $res) 222 | { 223 | $modelName = $res->getMatchedAgainst()->params[self::INJECT_VIEWMODEL_NAME]; 224 | return str_replace('|', '\\', $modelName); 225 | } 226 | 227 | /** 228 | * Get the request 229 | * 230 | * @return Request 231 | * @throws DomainException if unable to find request 232 | */ 233 | protected function getRequest() 234 | { 235 | if ($this->request) { 236 | return $this->request; 237 | } 238 | 239 | $event = $this->getEvent(); 240 | $request = $event->getRequest(); 241 | if (! $request instanceof Request) { 242 | throw new DomainException( 243 | 'The event used does not contain a valid Request, but must.' 244 | ); 245 | } 246 | 247 | $this->request = $request; 248 | return $request; 249 | } 250 | 251 | /** 252 | * Get the event 253 | * 254 | * @return MvcEvent 255 | * @throws DomainException if unable to find event 256 | */ 257 | protected function getEvent() 258 | { 259 | if ($this->event) { 260 | return $this->event; 261 | } 262 | 263 | $controller = $this->getController(); 264 | if (! $controller instanceof InjectApplicationEventInterface) { 265 | throw new DomainException( 266 | 'A controller that implements InjectApplicationEventInterface ' 267 | . 'is required to use ' . __CLASS__ 268 | ); 269 | } 270 | 271 | $event = $controller->getEvent(); 272 | if (! $event instanceof MvcEvent) { 273 | $params = $event->getParams(); 274 | $event = new MvcEvent(); 275 | $event->setParams($params); 276 | } 277 | $this->event = $event; 278 | 279 | return $this->event; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/View/Http/ViewManager.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], 10000); 77 | } 78 | 79 | /** 80 | * Prepares the view layer 81 | * 82 | * @param $event 83 | * @return void 84 | */ 85 | public function onBootstrap($event) 86 | { 87 | $application = $event->getApplication(); 88 | $services = $application->getServiceManager(); 89 | $config = $services->get('config'); 90 | $events = $application->getEventManager(); 91 | $sharedEvents = $events->getSharedManager(); 92 | 93 | $this->config = isset($config['view_manager']) 94 | && (is_array($config['view_manager']) 95 | || $config['view_manager'] instanceof ArrayAccess) 96 | ? $config['view_manager'] 97 | : []; 98 | $this->services = $services; 99 | $this->event = $event; 100 | 101 | $routeNotFoundStrategy = $services->get('HttpRouteNotFoundStrategy'); 102 | $exceptionStrategy = $services->get('HttpExceptionStrategy'); 103 | $mvcRenderingStrategy = $services->get('HttpDefaultRenderingStrategy'); 104 | 105 | $this->injectViewModelIntoPlugin(); 106 | 107 | $injectTemplateListener = $services->get('Zend\Mvc\View\Http\InjectTemplateListener'); 108 | $createViewModelListener = new CreateViewModelListener(); 109 | $injectViewModelListener = new InjectViewModelListener(); 110 | 111 | $this->registerMvcRenderingStrategies($events); 112 | $this->registerViewStrategies(); 113 | 114 | $routeNotFoundStrategy->attach($events); 115 | $exceptionStrategy->attach($events); 116 | $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$injectViewModelListener, 'injectViewModel'], -100); 117 | $events->attach(MvcEvent::EVENT_RENDER_ERROR, [$injectViewModelListener, 'injectViewModel'], -100); 118 | $mvcRenderingStrategy->attach($events); 119 | 120 | $sharedEvents->attach( 121 | 'Zend\Stdlib\DispatchableInterface', 122 | MvcEvent::EVENT_DISPATCH, 123 | [$createViewModelListener, 'createViewModelFromArray'], 124 | -80 125 | ); 126 | $sharedEvents->attach( 127 | 'Zend\Stdlib\DispatchableInterface', 128 | MvcEvent::EVENT_DISPATCH, 129 | [$routeNotFoundStrategy, 'prepareNotFoundViewModel'], 130 | -90 131 | ); 132 | $sharedEvents->attach( 133 | 'Zend\Stdlib\DispatchableInterface', 134 | MvcEvent::EVENT_DISPATCH, 135 | [$createViewModelListener, 'createViewModelFromNull'], 136 | -80 137 | ); 138 | $sharedEvents->attach( 139 | 'Zend\Stdlib\DispatchableInterface', 140 | MvcEvent::EVENT_DISPATCH, 141 | [$injectTemplateListener, 'injectTemplate'], 142 | -90 143 | ); 144 | $sharedEvents->attach( 145 | 'Zend\Stdlib\DispatchableInterface', 146 | MvcEvent::EVENT_DISPATCH, 147 | [$injectViewModelListener, 'injectViewModel'], 148 | -100 149 | ); 150 | } 151 | 152 | /** 153 | * Retrieves the View instance 154 | * 155 | * @return View 156 | */ 157 | public function getView() 158 | { 159 | if ($this->view) { 160 | return $this->view; 161 | } 162 | 163 | $this->view = $this->services->get(View::class); 164 | return $this->view; 165 | } 166 | 167 | /** 168 | * Configures the MvcEvent view model to ensure it has the template injected 169 | * 170 | * @return \Zend\View\Model\ModelInterface 171 | */ 172 | public function getViewModel() 173 | { 174 | if ($this->viewModel) { 175 | return $this->viewModel; 176 | } 177 | 178 | $this->viewModel = $model = $this->event->getViewModel(); 179 | $layoutTemplate = $this->services->get('HttpDefaultRenderingStrategy')->getLayoutTemplate(); 180 | $model->setTemplate($layoutTemplate); 181 | 182 | return $this->viewModel; 183 | } 184 | 185 | /** 186 | * Register additional mvc rendering strategies 187 | * 188 | * If there is a "mvc_strategies" key of the view manager configuration, loop 189 | * through it. Pull each as a service from the service manager, and, if it 190 | * is a ListenerAggregate, attach it to the view, at priority 100. This 191 | * latter allows each to trigger before the default mvc rendering strategy, 192 | * and for them to trigger in the order they are registered. 193 | * 194 | * @param EventManagerInterface $events 195 | * @return void 196 | */ 197 | protected function registerMvcRenderingStrategies(EventManagerInterface $events) 198 | { 199 | if (! isset($this->config['mvc_strategies'])) { 200 | return; 201 | } 202 | $mvcStrategies = $this->config['mvc_strategies']; 203 | if (is_string($mvcStrategies)) { 204 | $mvcStrategies = [$mvcStrategies]; 205 | } 206 | if (! is_array($mvcStrategies) && ! $mvcStrategies instanceof Traversable) { 207 | return; 208 | } 209 | 210 | foreach ($mvcStrategies as $mvcStrategy) { 211 | if (! is_string($mvcStrategy)) { 212 | continue; 213 | } 214 | 215 | $listener = $this->services->get($mvcStrategy); 216 | if ($listener instanceof ListenerAggregateInterface) { 217 | $listener->attach($events, 100); 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Register additional view strategies 224 | * 225 | * If there is a "strategies" key of the view manager configuration, loop 226 | * through it. Pull each as a service from the service manager, and, if it 227 | * is a ListenerAggregate, attach it to the view, at priority 100. This 228 | * latter allows each to trigger before the default strategy, and for them 229 | * to trigger in the order they are registered. 230 | * 231 | * @return void 232 | */ 233 | protected function registerViewStrategies() 234 | { 235 | if (! isset($this->config['strategies'])) { 236 | return; 237 | } 238 | $strategies = $this->config['strategies']; 239 | if (is_string($strategies)) { 240 | $strategies = [$strategies]; 241 | } 242 | if (! is_array($strategies) && ! $strategies instanceof Traversable) { 243 | return; 244 | } 245 | 246 | $view = $this->getView(); 247 | $events = $view->getEventManager(); 248 | 249 | foreach ($strategies as $strategy) { 250 | if (! is_string($strategy)) { 251 | continue; 252 | } 253 | 254 | $listener = $this->services->get($strategy); 255 | if ($listener instanceof ListenerAggregateInterface) { 256 | $listener->attach($events, 100); 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * Injects the ViewModel view helper with the root view model. 263 | */ 264 | private function injectViewModelIntoPlugin() 265 | { 266 | $model = $this->getViewModel(); 267 | $plugins = $this->services->get('ViewHelperManager'); 268 | $plugin = $plugins->get('viewmodel'); 269 | $plugin->setRoot($model); 270 | } 271 | } 272 | --------------------------------------------------------------------------------