PSR-7 native zend-mvc
This RFC covers high points of my ongoing effort to convert zend-mvc to PSR-7.
PSR-7 Request and Response
PSR-7 is a way into the future. But what does it mean for zend-mvc? Sadly, immutable nature is incompatible with zend-http and switch to PSR-7 constitutes immense backward compatibility break.
In versions 2 and 3, zend-mvc relies heavily on zend-http request and response mutability, where single instance is expected to be shared between many parts, starting from Zend\Mvc\Application
to various listeners, controllers, helpers.
To keep zend-mvc architecture changes to a minimum, mutability is shifted to MvcEvent instance, which then becomes a single source of truth throughout the dispatch. If request or response need to be changed, they should be obtained from MvcEvent, changed and then set back.
Console integration removal
Console and HTTP handling needs are different. Listeners and controllers are targeting one or the other most of the time, not both. In practice, many implementations do not expect to receive console requests at all. Besides, as stated above, PSR-7 request and response are not compatible with zend-stdlib request and response.
Considering all, support for console handling will be dropped from zend-mvc in version 4. That will simplify zend-mvc and allow to better focus on HTTP request handling.
Recommended way forward for framework users is to implement console as a separate application, utilizing shared container. It should be trivial with configuration merging and container setup moved out of zend-mvc.
zf-console is recommended for simple needs and Symfony console for more complex cases.
Depending on Request or Response outside of dispatching
Some zend-mvc users are depending on Request availability during bootstrap or even module loading.
This is flawed approach. Request and Response are runtime values and they are not guaranteed to exist or be the same during bootstrap. With container, service creation can happen outside of application runtime as well. To remove a possibility for their abuse, Request and Response are removed from container and application, and provided only during Zend\Mvc\Application::run()
as MvcEvent parameters.
Should request specific initialization be needed, it should happen early on mvc routing event.
Dedicated request setup event is likely to be introduced later specifically for that purpose.
ResponseSender replaced with Diactoros ResponseEmitter
Console removal allows zend-mvc to make use of Diactoros reusable PSR-7 response emitters and drop Zend\Mvc\ResponseSender
.
Zend\Mvc\Application
now composes emitter as a constructor dependency and invokes it in run()
. SendResponseListener is removed from finish
event and dropped.
Mvc Application as a PSR-15 RequestHandler
Zend\Mvc\Application
implements PSR RequestHandler so it could be used as a final handler, for example in middleware pipelines.
Zend\Mvc\Application::run()
delegates to Zend\Mvc\Application::handle()
and emits returned Response with the help of ResponseEmitter.
Since handle()
must return response and not emit it, SendResponseListener is removed from 'finish` mvc event.
ModuleManager is… gone?
Back in the time, module manager was introduced to solve basic packaging support, autoloading, config loading and merging. Number of important things happened since then.
Composer emerged and became a standard package manager in php world. It solved all our autoloading needs. No longer constrained by autoloading, ConfigAggregator was introduced to handle config loading and merging in a simple and clean way.
Service configuration is available before container is created, Zend\ModuleManager\Listener\ServiceListener
for various plugin managers could be replaced by regular factories. That fits well with zend-servicemanager v3 push for immutability.
Module::onBootstrap()
is just a convenience method for providing listener for zend-mvc bootstrap event. Zend\Mvc\Application
factory is now looks for ListenerAggregates in config which then attached to Application’s EventManager.
Dropping module manager from zend-mvc greatly reduces Application complexity, makes zend-mvc more consistent with zend-expressive and ConfigProvider use in zend framework components.
New mvc application setup would be immediately familiar for zend-expressive users:
// config/config.php
$aggregator = new ConfigAggregator([
// ...
Zend\Mvc\ConfigProvider::class,
Zend\Router\ConfigProvider::class,
// Default App module config
Application\ConfigProvider::class,
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
// ...
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();
// public/index.php
// ...
chdir(dirname(__DIR__));
require 'vendor/autoload.php';
(function () {
$container = require 'config/container.php';
$app = $container->get(\Zend\Mvc\Application::class);
$app->run();
})();
Controller plugins deprecation proposal
This RFC proposes to deprecate controller plugins and to remove plugin manager from abstract controllers.
Controller plugins (previously helpers) originate in pre-container times, when injecting dependencies was hard. It is no longer true.
Controller plugins provided via service locator obscure dependencies. Non-enforceable expectations of plugin behavior and interface create worst kind of implicit dependency entanglement. Mere presence of plugin manager in controllers erodes unit boundaries, making harder to unit test controllers.
By design, plugins provide side-effect based behavior. For example, createNotFoundViewModel sets 404 status in controller’s Response instance. Oops.
Zend-expressive is doing well without such concept and I believe it will be beneficial for ZF users as it will encourage them to switch to dependency injection if they didn’t already.
As a side effect, deprecation will allow us to repurpose existing plugins and plugin manager to provide backward compatibility layer for zend-http based v3 controllers.
Controllers as PSR-15 Request Handlers proposal
This RFC proposes to change zend-mvc controller to be a PSR-15 request handler. Requiring controller to always return Response, as per interface, will make it ultimate authority in handling request and preparing response and will grant it more control.
Implementation wise it will mean additional render
event in AbstractController and its descendants. Existing event powered flexibility will not be lost: rendering listeners will also attach to shared event manager on identifier Zend\Mvc\Controller\ControllerInterface
that is going to replace PSR-7 incompatible Zend\Stdlib\DispatchableInterface
.
For AbstractController, dispatch
event listeners will not be affected. As a consequence AbstractActionController actions will not be affected as well: they will still be able to return values, view models or Response object.
I believe, rendering happening within controller events will make zend-mvc event system easier to understand for users, greatly improving on learning curve.
Request handlers that are not zend-mvc controllers will be free to use any renderer, if any.
To improve user experience, fallback EventManager will not be created in controllers to force its injection and helpful errors:
public function handle(Request $request) : ResponseInterface
{
$events = $this->getEventManager();
if (! $events) {
throw new RuntimeException('Controller %s requires EventManager with SharedEventManager provided by zend-mvc application to be composed');
}
// ...
}
Additionally, in case response is not provided after dispatch
and render
controller events, event manager will be inspected for shared manager and presence of zend-mvc listeners to provide helpful feedback.
Create new mvc skeleton application
Documentation for older versions of framework reference composer create-project zendframework/skeleton-application
without version constraint and then tutorial proceeds to give instructions for version 2.4 while installed skeleton is actually for mvc v3. That creates a lot of confusion for new users. I propose to create new zendframework/zend-mvc-skeleton
repository and package for v4 to avoid further confusion and to be more in line with the rest of the framework.