Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit a84894b

Browse files
committed
Creates new delegator factory, ApplicationConfigInjectionDelegator
This delegator factory decorates the application instance created by the callback by injecting a pipeline and routes based on configuration pulled from the container. The functionality is directly from the ApplicationConfigInjectionTrait, which we can deprecate and remove now that this functionality is in place.
1 parent a5ed347 commit a84894b

File tree

2 files changed

+766
-0
lines changed

2 files changed

+766
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
4+
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Zend\Expressive\Container;
11+
12+
use Psr\Container\ContainerInterface;
13+
use SplPriorityQueue;
14+
use Zend\Expressive\Application;
15+
use Zend\Expressive\Exception\InvalidArgumentException;
16+
use Zend\Expressive\Middleware;
17+
use Zend\Expressive\Router\Route;
18+
19+
class ApplicationConfigInjectionDelegator
20+
{
21+
/**
22+
* Decorate an Application instance by injecting routes and/or middleware
23+
* from configuration.
24+
*/
25+
public function __invoke(ContainerInterface $container, string $serviceName, callable $callback) : Application
26+
{
27+
$application = $callback();
28+
if (! $application instanceof Application) {
29+
return $application;
30+
}
31+
32+
if (! $container->has('config')) {
33+
return $application;
34+
}
35+
36+
$config = $container->get('config');
37+
if (! isset($config['routes']) && ! isset($config['middleware_pipeline'])) {
38+
return $application;
39+
}
40+
41+
$this->injectPipelineFromConfig($application, $config);
42+
$this->injectRoutesFromConfig($application, $config);
43+
44+
return $application;
45+
}
46+
47+
/**
48+
* Inject a middleware pipeline from the middleware_pipeline configuration.
49+
*
50+
* Inspects the configuration provided to determine if a middleware pipeline
51+
* exists to inject in the application.
52+
*
53+
* If no pipeline is defined, but routes *are*, then the method will inject
54+
* the routing and dispatch middleware.
55+
*
56+
* Use the following configuration format:
57+
*
58+
* <code>
59+
* return [
60+
* 'middleware_pipeline' => [
61+
* // An array of middleware to register with the pipeline.
62+
* // entries to register prior to routing/dispatching...
63+
* // - entry for \Zend\Expressive\Middleware\RouteMiddleware::class
64+
* // - entry \Zend\Expressive\Middleware\DispatchMiddleware::class
65+
* // entries to register after routing/dispatching...
66+
* ],
67+
* ];
68+
* </code>
69+
*
70+
* Each item in the middleware_pipeline array must be of the following
71+
* specification:
72+
*
73+
* <code>
74+
* [
75+
* // required:
76+
* 'middleware' => 'Name of middleware service, or a callable',
77+
* // optional:
78+
* 'path' => '/path/to/match',
79+
* 'priority' => 1, // integer
80+
* ]
81+
* </code>
82+
*
83+
* Note that the `path` element can only be a literal.
84+
*
85+
* `priority` is used to shape the order in which middleware is piped to the
86+
* application. Values are integers, with high values having higher priority
87+
* (piped earlier), and low/negative values having lower priority (piped last).
88+
* Default priority if none is specified is 1. Middleware with the same
89+
* priority are piped in the order in which they appear.
90+
*
91+
* Middleware piped may be either callables or service names.
92+
*
93+
* Additionally, you can specify an array of callables or service names as
94+
* the `middleware` value of a specification. Internally, this will create
95+
* a `Zend\Stratigility\MiddlewarePipe` instance, with the middleware
96+
* specified piped in the order provided.
97+
*/
98+
public function injectPipelineFromConfig(Application $application, array $config) : void
99+
{
100+
if (empty($config['middleware_pipeline'])) {
101+
if (! isset($config['routes']) || ! is_array($config['routes'])) {
102+
return;
103+
}
104+
105+
$application->pipe(Middleware\RouteMiddleware::class);
106+
$application->pipe(Middleware\DispatchMiddleware::class);
107+
return;
108+
}
109+
110+
// Create a priority queue from the specifications
111+
$queue = array_reduce(
112+
array_map(self::createCollectionMapper(), $config['middleware_pipeline']),
113+
self::createPriorityQueueReducer(),
114+
new SplPriorityQueue()
115+
);
116+
117+
foreach ($queue as $spec) {
118+
$path = $spec['path'] ?? '/';
119+
$application->pipe($path, $spec['middleware']);
120+
}
121+
}
122+
123+
/**
124+
* Inject routes from configuration.
125+
*
126+
* Introspects the provided configuration for routes to inject in the
127+
* application instance.
128+
*
129+
* The following configuration structure can be used to define routes:
130+
*
131+
* <code>
132+
* return [
133+
* 'routes' => [
134+
* [
135+
* 'path' => '/path/to/match',
136+
* 'middleware' => 'Middleware Service Name or Callable',
137+
* 'allowed_methods' => ['GET', 'POST', 'PATCH'],
138+
* 'options' => [
139+
* 'stuff' => 'to',
140+
* 'pass' => 'to',
141+
* 'the' => 'underlying router',
142+
* ],
143+
* ],
144+
* // etc.
145+
* ],
146+
* ];
147+
* </code>
148+
*
149+
* Each route MUST have a path and middleware key at the minimum.
150+
*
151+
* The "allowed_methods" key may be omitted, can be either an array or the
152+
* value of the Zend\Expressive\Router\Route::HTTP_METHOD_ANY constant; any
153+
* valid HTTP method token is allowed, which means you can specify custom HTTP
154+
* methods as well.
155+
*
156+
* The "options" key may also be omitted, and its interpretation will be
157+
* dependent on the underlying router used.
158+
*
159+
* @throws InvalidArgumentException
160+
*/
161+
public function injectRoutesFromConfig(Application $application, array $config) : void
162+
{
163+
if (! isset($config['routes']) || ! is_array($config['routes'])) {
164+
return;
165+
}
166+
167+
foreach ($config['routes'] as $spec) {
168+
if (! isset($spec['path']) || ! isset($spec['middleware'])) {
169+
continue;
170+
}
171+
172+
$methods = Route::HTTP_METHOD_ANY;
173+
if (isset($spec['allowed_methods'])) {
174+
$methods = $spec['allowed_methods'];
175+
if (! is_array($methods)) {
176+
throw new InvalidArgumentException(sprintf(
177+
'Allowed HTTP methods for a route must be in form of an array; received "%s"',
178+
gettype($methods)
179+
));
180+
}
181+
}
182+
183+
$name = $spec['name'] ?? null;
184+
$route = $application->route(
185+
$spec['path'],
186+
$spec['middleware'],
187+
$methods,
188+
$name
189+
);
190+
191+
if (isset($spec['options'])) {
192+
$options = $spec['options'];
193+
if (! is_array($options)) {
194+
throw new InvalidArgumentException(sprintf(
195+
'Route options must be an array; received "%s"',
196+
gettype($options)
197+
));
198+
}
199+
200+
$route->setOptions($options);
201+
}
202+
}
203+
}
204+
205+
/**
206+
* Create the collection mapping function.
207+
*
208+
* Returns a callable with the following signature:
209+
*
210+
* <code>
211+
* function (array|string $item) : array
212+
* </code>
213+
*
214+
* When it encounters one of the self::*_MIDDLEWARE constants, it passes
215+
* the value to the `createPipelineMapper()` callback to create a spec
216+
* that uses the return value as pipeline middleware.
217+
*
218+
* If the 'middleware' value is an array, it uses the `createPipelineMapper()`
219+
* callback as an array mapper in order to ensure the self::*_MIDDLEWARE
220+
* are injected correctly.
221+
*
222+
* If the 'middleware' value is missing, or not viable as middleware, it
223+
* raises an exception, to ensure the pipeline is built correctly.
224+
*
225+
* @throws InvalidArgumentException
226+
*/
227+
private static function createCollectionMapper() : callable
228+
{
229+
return function ($item) {
230+
if (! is_array($item) || ! array_key_exists('middleware', $item)) {
231+
throw new InvalidArgumentException(sprintf(
232+
'Invalid pipeline specification received; must be an array containing a middleware '
233+
. 'key, or one of the Application::*_MIDDLEWARE constants; received %s',
234+
is_object($item) ? get_class($item) : gettype($item)
235+
));
236+
}
237+
238+
return $item;
239+
};
240+
}
241+
242+
/**
243+
* Create reducer function that will reduce an array to a priority queue.
244+
*
245+
* Creates and returns a function with the signature:
246+
*
247+
* <code>
248+
* function (SplQueue $queue, array $item) : SplQueue
249+
* </code>
250+
*
251+
* The function is useful to reduce an array of pipeline middleware to a
252+
* priority queue.
253+
*/
254+
private static function createPriorityQueueReducer() : callable
255+
{
256+
// $serial is used to ensure that items of the same priority are enqueued
257+
// in the order in which they are inserted.
258+
$serial = PHP_INT_MAX;
259+
return function ($queue, $item) use (&$serial) {
260+
$priority = isset($item['priority']) && is_int($item['priority'])
261+
? $item['priority']
262+
: 1;
263+
$queue->insert($item, [$priority, $serial]);
264+
$serial -= 1;
265+
return $queue;
266+
};
267+
}
268+
}

0 commit comments

Comments
 (0)