Passed
Branch master (3c33c7)
by Andrey
04:31
created

HandlerList::resolve()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 5
nop 0
dl 0
loc 15
rs 10
1
<?php
2
namespace Aws;
3
4
/**
5
 * Builds a single handler function from zero or more middleware functions and
6
 * a handler. The handler function is then used to send command objects and
7
 * return a promise that is resolved with an AWS result object.
8
 *
9
 * The "front" of the list is invoked before the "end" of the list. You can add
10
 * middleware to the front of the list using one of the "prepend" method, and
11
 * the end of the list using one of the "append" method. The last function
12
 * invoked in a handler list is the handler (a function that does not accept a
13
 * next handler but rather is responsible for returning a promise that is
14
 * fulfilled with an Aws\ResultInterface object).
15
 *
16
 * Handlers are ordered using a "step" that describes the step at which the
17
 * SDK is when sending a command. The available steps are:
18
 *
19
 * - init: The command is being initialized, allowing you to do things like add
20
 *   default options.
21
 * - validate: The command is being validated before it is serialized
22
 * - build: The command is being serialized into an HTTP request. A middleware
23
 *   in this step MUST serialize an HTTP request and populate the "@request"
24
 *   parameter of a command with the request such that it is available to
25
 *   subsequent middleware.
26
 * - sign: The request is being signed and prepared to be sent over the wire.
27
 *
28
 * Middleware can be registered with a name to allow you to easily add a
29
 * middleware before or after another middleware by name. This also allows you
30
 * to remove a middleware by name (in addition to removing by instance).
31
 */
32
class HandlerList implements \Countable
33
{
34
    const INIT = 'init';
35
    const VALIDATE = 'validate';
36
    const BUILD = 'build';
37
    const SIGN = 'sign';
38
39
    /** @var callable */
40
    private $handler;
41
42
    /** @var array */
43
    private $named = [];
44
45
    /** @var array */
46
    private $sorted;
47
48
    /** @var callable|null */
49
    private $interposeFn;
50
51
    /** @var array Steps (in reverse order) */
52
    private $steps = [
53
        self::SIGN     => [],
54
        self::BUILD    => [],
55
        self::VALIDATE => [],
56
        self::INIT     => [],
57
    ];
58
59
    /**
60
     * @param callable $handler HTTP handler.
61
     */
62
    public function __construct(callable $handler = null)
63
    {
64
        $this->handler = $handler;
65
    }
66
67
    /**
68
     * Dumps a string representation of the list.
69
     *
70
     * @return string
71
     */
72
    public function __toString()
73
    {
74
        $str = '';
75
        $i = 0;
76
77
        foreach (array_reverse($this->steps) as $k => $step) {
78
            foreach (array_reverse($step) as $j => $tuple) {
79
                $str .= "{$i}) Step: {$k}, ";
80
                if ($tuple[1]) {
81
                    $str .= "Name: {$tuple[1]}, ";
82
                }
83
                $str .= "Function: " . $this->debugCallable($tuple[0]) . "\n";
84
                $i++;
85
            }
86
        }
87
88
        if ($this->handler) {
89
            $str .= "{$i}) Handler: " . $this->debugCallable($this->handler) . "\n";
90
        }
91
92
        return $str;
93
    }
94
95
    /**
96
     * Set the HTTP handler that actually returns a response.
97
     *
98
     * @param callable $handler Function that accepts a request and array of
99
     *                          options and returns a Promise.
100
     */
101
    public function setHandler(callable $handler)
102
    {
103
        $this->handler = $handler;
104
    }
105
106
    /**
107
     * Returns true if the builder has a handler.
108
     *
109
     * @return bool
110
     */
111
    public function hasHandler()
112
    {
113
        return (bool) $this->handler;
114
    }
115
116
    /**
117
     * Append a middleware to the init step.
118
     *
119
     * @param callable $middleware Middleware function to add.
120
     * @param string   $name       Name of the middleware.
121
     */
122
    public function appendInit(callable $middleware, $name = null)
123
    {
124
        $this->add(self::INIT, $name, $middleware);
125
    }
126
127
    /**
128
     * Prepend a middleware to the init step.
129
     *
130
     * @param callable $middleware Middleware function to add.
131
     * @param string   $name       Name of the middleware.
132
     */
133
    public function prependInit(callable $middleware, $name = null)
134
    {
135
        $this->add(self::INIT, $name, $middleware, true);
136
    }
137
138
    /**
139
     * Append a middleware to the validate step.
140
     *
141
     * @param callable $middleware Middleware function to add.
142
     * @param string   $name       Name of the middleware.
143
     */
144
    public function appendValidate(callable $middleware, $name = null)
145
    {
146
        $this->add(self::VALIDATE, $name, $middleware);
147
    }
148
149
    /**
150
     * Prepend a middleware to the validate step.
151
     *
152
     * @param callable $middleware Middleware function to add.
153
     * @param string   $name       Name of the middleware.
154
     */
155
    public function prependValidate(callable $middleware, $name = null)
156
    {
157
        $this->add(self::VALIDATE, $name, $middleware, true);
158
    }
159
160
    /**
161
     * Append a middleware to the build step.
162
     *
163
     * @param callable $middleware Middleware function to add.
164
     * @param string   $name       Name of the middleware.
165
     */
166
    public function appendBuild(callable $middleware, $name = null)
167
    {
168
        $this->add(self::BUILD, $name, $middleware);
169
    }
170
171
    /**
172
     * Prepend a middleware to the build step.
173
     *
174
     * @param callable $middleware Middleware function to add.
175
     * @param string   $name       Name of the middleware.
176
     */
177
    public function prependBuild(callable $middleware, $name = null)
178
    {
179
        $this->add(self::BUILD, $name, $middleware, true);
180
    }
181
182
    /**
183
     * Append a middleware to the sign step.
184
     *
185
     * @param callable $middleware Middleware function to add.
186
     * @param string   $name       Name of the middleware.
187
     */
188
    public function appendSign(callable $middleware, $name = null)
189
    {
190
        $this->add(self::SIGN, $name, $middleware);
191
    }
192
193
    /**
194
     * Prepend a middleware to the sign step.
195
     *
196
     * @param callable $middleware Middleware function to add.
197
     * @param string   $name       Name of the middleware.
198
     */
199
    public function prependSign(callable $middleware, $name = null)
200
    {
201
        $this->add(self::SIGN, $name, $middleware, true);
202
    }
203
204
    /**
205
     * Add a middleware before the given middleware by name.
206
     *
207
     * @param string|callable $findName   Add before this
208
     * @param string          $withName   Optional name to give the middleware
209
     * @param callable        $middleware Middleware to add.
210
     */
211
    public function before($findName, $withName, callable $middleware)
212
    {
213
        $this->splice($findName, $withName, $middleware, true);
214
    }
215
216
    /**
217
     * Add a middleware after the given middleware by name.
218
     *
219
     * @param string|callable $findName   Add after this
220
     * @param string          $withName   Optional name to give the middleware
221
     * @param callable        $middleware Middleware to add.
222
     */
223
    public function after($findName, $withName, callable $middleware)
224
    {
225
        $this->splice($findName, $withName, $middleware, false);
226
    }
227
228
    /**
229
     * Remove a middleware by name or by instance from the list.
230
     *
231
     * @param string|callable $nameOrInstance Middleware to remove.
232
     */
233
    public function remove($nameOrInstance)
234
    {
235
        if (is_callable($nameOrInstance)) {
236
            $this->removeByInstance($nameOrInstance);
237
        } elseif (is_string($nameOrInstance)) {
238
            $this->removeByName($nameOrInstance);
239
        }
240
    }
241
242
    /**
243
     * Interpose a function between each middleware (e.g., allowing for a trace
244
     * through the middleware layers).
245
     *
246
     * The interpose function is a function that accepts a "step" argument as a
247
     * string and a "name" argument string. This function must then return a
248
     * function that accepts the next handler in the list. This function must
249
     * then return a function that accepts a CommandInterface and optional
250
     * RequestInterface and returns a promise that is fulfilled with an
251
     * Aws\ResultInterface or rejected with an Aws\Exception\AwsException
252
     * object.
253
     *
254
     * @param callable|null $fn Pass null to remove any previously set function
255
     */
256
    public function interpose(callable $fn = null)
257
    {
258
        $this->sorted = null;
259
        $this->interposeFn = $fn;
260
    }
261
262
    /**
263
     * Compose the middleware and handler into a single callable function.
264
     *
265
     * @return callable
266
     */
267
    public function resolve()
268
    {
269
        if (!($prev = $this->handler)) {
270
            throw new \LogicException('No handler has been specified');
271
        }
272
273
        if ($this->sorted === null) {
274
            $this->sortMiddleware();
275
        }
276
277
        foreach ($this->sorted as $fn) {
278
            $prev = $fn($prev);
279
        }
280
281
        return $prev;
282
    }
283
284
    public function count()
285
    {
286
        return count($this->steps[self::INIT])
287
            + count($this->steps[self::VALIDATE])
288
            + count($this->steps[self::BUILD])
289
            + count($this->steps[self::SIGN]);
290
    }
291
292
    /**
293
     * Splices a function into the middleware list at a specific position.
294
     *
295
     * @param          $findName
296
     * @param          $withName
297
     * @param callable $middleware
298
     * @param          $before
299
     */
300
    private function splice($findName, $withName, callable $middleware, $before)
301
    {
302
        if (!isset($this->named[$findName])) {
303
            throw new \InvalidArgumentException("$findName not found");
304
        }
305
306
        $idx = $this->sorted = null;
307
        $step = $this->named[$findName];
308
309
        if ($withName) {
310
            $this->named[$withName] = $step;
311
        }
312
313
        foreach ($this->steps[$step] as $i => $tuple) {
314
            if ($tuple[1] === $findName) {
315
                $idx = $i;
316
                break;
317
            }
318
        }
319
320
        $replacement = $before
321
            ? [$this->steps[$step][$idx], [$middleware, $withName]]
322
            : [[$middleware, $withName], $this->steps[$step][$idx]];
323
        array_splice($this->steps[$step], $idx, 1, $replacement);
324
    }
325
326
    /**
327
     * Provides a debug string for a given callable.
328
     *
329
     * @param array|callable $fn Function to write as a string.
330
     *
331
     * @return string
332
     */
333
    private function debugCallable($fn)
334
    {
335
        if (is_string($fn)) {
336
            return "callable({$fn})";
337
        } elseif (is_array($fn)) {
338
            $ele = is_string($fn[0]) ? $fn[0] : get_class($fn[0]);
339
            return "callable(['{$ele}', '{$fn[1]}'])";
340
        } else {
341
            return 'callable(' . spl_object_hash($fn) . ')';
0 ignored issues
show
Bug introduced by
$fn of type callable is incompatible with the type object expected by parameter $object of spl_object_hash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

341
            return 'callable(' . spl_object_hash(/** @scrutinizer ignore-type */ $fn) . ')';
Loading history...
342
        }
343
    }
344
345
    /**
346
     * Sort the middleware, and interpose if needed in the sorted list.
347
     */
348
    private function sortMiddleware()
349
    {
350
        $this->sorted = [];
351
352
        if (!$this->interposeFn) {
353
            foreach ($this->steps as $step) {
354
                foreach ($step as $fn) {
355
                    $this->sorted[] = $fn[0];
356
                }
357
            }
358
            return;
359
        }
360
361
        $ifn = $this->interposeFn;
362
        // Interpose the interposeFn into the handler stack.
363
        foreach ($this->steps as $stepName => $step) {
364
            foreach ($step as $fn) {
365
                $this->sorted[] = $ifn($stepName, $fn[1]);
366
                $this->sorted[] = $fn[0];
367
            }
368
        }
369
    }
370
371
    private function removeByName($name)
372
    {
373
        if (!isset($this->named[$name])) {
374
            return;
375
        }
376
377
        $this->sorted = null;
378
        $step = $this->named[$name];
379
        $this->steps[$step] = array_values(
380
            array_filter(
381
                $this->steps[$step],
382
                function ($tuple) use ($name) {
383
                    return $tuple[1] !== $name;
384
                }
385
            )
386
        );
387
    }
388
389
    private function removeByInstance(callable $fn)
390
    {
391
        foreach ($this->steps as $k => $step) {
392
            foreach ($step as $j => $tuple) {
393
                if ($tuple[0] === $fn) {
394
                    $this->sorted = null;
395
                    unset($this->named[$this->steps[$k][$j][1]]);
396
                    unset($this->steps[$k][$j]);
397
                }
398
            }
399
        }
400
    }
401
402
    /**
403
     * Add a middleware to a step.
404
     *
405
     * @param string   $step       Middleware step.
406
     * @param string   $name       Middleware name.
407
     * @param callable $middleware Middleware function to add.
408
     * @param bool     $prepend    Prepend instead of append.
409
     */
410
    private function add($step, $name, callable $middleware, $prepend = false)
411
    {
412
        $this->sorted = null;
413
414
        if ($prepend) {
415
            $this->steps[$step][] = [$middleware, $name];
416
        } else {
417
            array_unshift($this->steps[$step], [$middleware, $name]);
418
        }
419
420
        if ($name) {
421
            $this->named[$name] = $step;
422
        }
423
    }
424
}
425