The various use case notes below were brainstorming ideas that were then used to create test cases and help guide implementation. In the end, direct instantiation of Application was undesirable in order to promote proper IoC; this was when AppFactory was introduced.
Consider them an historical record, and not actual usage examples; those can be found in doc/book/usage-examples.md at this time.
The sections on "Templated Middleware", "Middleware for any method", and "Design concerns" remain relevant still, and detail decisions made or still in progress.
<?php
use Zend\Expressive\Application;
require __DIR__ . '/../vendor/autoload.php';
$app = new Application();
$app->get('/', function ($req, $res, $next) {
$res->getBody()->write('Hello, world!');
return $res;
});
$app->run();
public/index.php
:
<?php
use Zend\ServiceManager\Config;
use Zend\ServiceManager\ServiceManager;
require __DIR__ . '/../vendor/autoload.php';
$services = new ServiceManager();
$config = new Config(require 'config/services.php');
$config->configureServiceManager($services);
$app = $services->get('Zend\Expressive\Application');
$app->run();
config/services.php
:
<?php
return [
'factories' => [
'Zend\Expressive\Application' => 'Application\ApplicationFactory',
],
];
src/ApplicationFactory.php
:
<?php
namespace Application;
use Zend\Expressive\Application;
class ApplicationFactory
{
public function __invoke($services)
{
$app = new Application();
// Setup the application programatically within the factory
$app->get('/', function ($req, $res, $next) {
$res->getBody()->write('Hello, world!');
return $res;
});
return $app;
}
}
public/index.php
:
<?php
use Zend\ServiceManager\Config;
use Zend\ServiceManager\ServiceManager;
require __DIR__ . '/../vendor/autoload.php';
$services = new ServiceManager();
$config = new Config(require 'config/services.php');
$config->configureServiceManager($services);
$app = $services->get('Zend\Expressive\Application');
$app->run();
config/services.php
:
<?php
return [
'services' => [
'config' => require __DIR__ . '/config.php',
],
'factories' => [
'Application\Middleware\HelloWorld' => 'Application\Middleware\HelloWorldFactory',
'Zend\Expressive\Application' => 'Application\ApplicationFactory',
'Zend\Expressive\Router\RouterInterface' => 'Application\RouterFactory',
],
];
config/config.php
:
<?php
return [
'routes' => [
'home' => [
'url' => '/',
'middleware' => 'Application\Middleware\HelloWorld',
],
],
];
src/RouterFactory.php
:
<?php
namespace Application;
use Zend\Expressive\Router\Aura as AuraRouter;
class RouterFactory
{
public function __invoke($services)
{
$config = $services->has('config') ? $services->get('config') : [];
$router = new AuraRouter();
$router->setConfig($config);
return $router;
}
}
src/ApplicationFactory.php
:
<?php
namespace Application;
use Zend\Expressive\Application;
class ApplicationFactory
{
public function __invoke($services)
{
// Router injected at instantiation
$router = $services->get('Zend\Expressive\Router\RouterInterface');
return new Application($router);
}
}
src\Middleware\HelloWorldFactory
:
<?php
namespace Application\Middleware;
class HelloWorldFactory
{
public function __invoke($services)
{
// Returning a class instance:
return new HelloWorld();
// or returning a closure:
return function ($req, $res, $next) {
$res->getBody()->write('Hello, world!');
return $res;
};
}
}
Same example as above, but we'll add more routes in the application factory.
<?php
namespace Application;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Application;
class ApplicationFactory
{
public function __invoke($services)
{
// Router injected at instantiation
$router = $services->get('Zend\Expressive\RouterInterface');
$app = new Application($router);
$app->get('/ping', function ($req, $res, $next) {
return new JsonResponse(['ack' => time()]);
});
return $app;
}
}
I'd originally thought we could return a view model, but that breaks the middleware contract. Instead, my thought is one of the following:
- "Templated" response that has no renderer. A "Templated response emitter" would take the response metadata, pass it to a template renderer, and write to the response to return it.
<?php
// middleware would do this:
$middleware = function ($req, $res, $next) {
return new TemplatedResponse($template, $variables);
};
// Emitter might do this:
class TemplatedResponseEmitter
{
/**
* We'd have to typehint on the PSR-7 interface, but this is just a simple
* illustration of the workflow.
*/
public function emit(TemplatedResponse $response)
{
$content = $this->renderer->render($response->getTemplate, $response->getVariables());
// This is operating under the assumption of a two-pass render such as
// ZF2's PhpRenderer. Systems such as phly/mustache, league/plates, and
// twig allow inheritance, which would obviate the need for this.
if ($this->hasLayout()) {
$content = $this->renderer->render($this->getLayout(), [
$this->getContentKey() => $content,
]);
}
$response->getBody()->write($content);
$this->parent->emit($response);
}
}
- Or, similarly, the templated response emitter would inject the stream with the renderer prior to attempting to emit the response; the act of injection would render the template and populate the stream.
<?php
// middleware would do this:
$middleware = function ($req, $res, $next) {
return new TemplatedResponse($template, $variables);
};
// Emitter might do this:
class TemplatedResponseEmitter
{
/**
* We'd have to typehint on the PSR-7 interface, but this is just a simple
* illustration of the workflow.
*/
public function emit(TemplatedResponse $response)
{
$response->setRenderer($this->renderer);
$this->parent->emit($response);
}
}
- Alternately, and more simply, the middleware can be injected with the template renderer, and the onus is on the user to render the template into the response and return the response.
<?php
class CustomMiddleware
{
private $renderer;
public function __construct(RendererInterface $renderer)
{
$this->renderer = $renderer;
}
public function __invoke($req, $res, $next)
{
$res->getBody()->write($this->renderer->render('some/template', ['some' => 'vars']));
return $res;
// or:
return new HtmlResponse($this->renderer->render(
'some/template',
['some' => 'vars']
));
}
}
My feeling is that the last is simplest from each of an implementation and usability standpoint. However, if we go this route, we will need to provide:
- An abstract class that accepts the template renderer via the constructor or a setter, and/or an "Aware" interface.
- A reusable factory that templated middleware can use that will inject the template renderer, and/or a delegator factory, so that users will not be required to write such a factory.
My inclination is to use interface injection here.
Middleware for any method is already possible, using pipe()
. However, we would
want to overload this in the Application
class such that it creates a route
definition. In order to keep the same semantics, I suggest:
route($routeOrPath, $middleware = null, array $methods = null)
. Given aRoute
instance, it just attaches that route. Given the path and middleware, it creates aRoute
instance that can listen on any HTTP method; providing an array of$methods
will limit to those methods.- The various HTTP-method Application methods would delegate to
route()
.
This means that a route will minimally contain:
- URL (what needs to match in the URI to execute the middleware)
- Middleware (a callable or service name for the middleware to execute on match)
- HTTP methods the middleware can handle.
Additionally, it MAY need:
- Options (any other options/metadata regarding the route to pass on to the router)
Finally, by having route()
return the Route
instance, the user can further
customize it. I would argue that only options be mutable, however, as the
combination of path + HTTP method is what determines whether or not routes have
conflicts.
-
How do we allow attaching middleware to execute on every request?
The simplest solution is to not handle it in the
Application
. The reason is simple: otherwise we have to worry about when the dispatcher is registered with the pipeline. If we do it at instantiation, we cannot have middleware intercept prior to the dispatcher; if we do it at invocation, we cannot have middleware or error middleware that executes after. Sincepipe()
has no concept of priority, and is simply a queue, the ony solution that will give consistent results is:-
Register the dispatcher middleware at instantiation
-
Require that users compose an
Application
in anotherMiddlewarePipe
if they want pre/post middleware:
-