1<?php
2namespace 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 */
32class HandlerList implements \Countable
33{
34    const INIT = 'init';
35    const VALIDATE = 'validate';
36    const BUILD = 'build';
37    const SIGN = 'sign';
38    const ATTEMPT = 'attempt';
39
40    /** @var callable */
41    private $handler;
42
43    /** @var array */
44    private $named = [];
45
46    /** @var array */
47    private $sorted;
48
49    /** @var callable|null */
50    private $interposeFn;
51
52    /** @var array Steps (in reverse order) */
53    private $steps = [
54        self::ATTEMPT  => [],
55        self::SIGN     => [],
56        self::BUILD    => [],
57        self::VALIDATE => [],
58        self::INIT     => [],
59    ];
60
61    /**
62     * @param callable $handler HTTP handler.
63     */
64    public function __construct(callable $handler = null)
65    {
66        $this->handler = $handler;
67    }
68
69    /**
70     * Dumps a string representation of the list.
71     *
72     * @return string
73     */
74    public function __toString()
75    {
76        $str = '';
77        $i = 0;
78
79        foreach (array_reverse($this->steps) as $k => $step) {
80            foreach (array_reverse($step) as $j => $tuple) {
81                $str .= "{$i}) Step: {$k}, ";
82                if ($tuple[1]) {
83                    $str .= "Name: {$tuple[1]}, ";
84                }
85                $str .= "Function: " . $this->debugCallable($tuple[0]) . "\n";
86                $i++;
87            }
88        }
89
90        if ($this->handler) {
91            $str .= "{$i}) Handler: " . $this->debugCallable($this->handler) . "\n";
92        }
93
94        return $str;
95    }
96
97    /**
98     * Set the HTTP handler that actually returns a response.
99     *
100     * @param callable $handler Function that accepts a request and array of
101     *                          options and returns a Promise.
102     */
103    public function setHandler(callable $handler)
104    {
105        $this->handler = $handler;
106    }
107
108    /**
109     * Returns true if the builder has a handler.
110     *
111     * @return bool
112     */
113    public function hasHandler()
114    {
115        return (bool) $this->handler;
116    }
117
118    /**
119     * Append a middleware to the init step.
120     *
121     * @param callable $middleware Middleware function to add.
122     * @param string   $name       Name of the middleware.
123     */
124    public function appendInit(callable $middleware, $name = null)
125    {
126        $this->add(self::INIT, $name, $middleware);
127    }
128
129    /**
130     * Prepend a middleware to the init step.
131     *
132     * @param callable $middleware Middleware function to add.
133     * @param string   $name       Name of the middleware.
134     */
135    public function prependInit(callable $middleware, $name = null)
136    {
137        $this->add(self::INIT, $name, $middleware, true);
138    }
139
140    /**
141     * Append a middleware to the validate step.
142     *
143     * @param callable $middleware Middleware function to add.
144     * @param string   $name       Name of the middleware.
145     */
146    public function appendValidate(callable $middleware, $name = null)
147    {
148        $this->add(self::VALIDATE, $name, $middleware);
149    }
150
151    /**
152     * Prepend a middleware to the validate step.
153     *
154     * @param callable $middleware Middleware function to add.
155     * @param string   $name       Name of the middleware.
156     */
157    public function prependValidate(callable $middleware, $name = null)
158    {
159        $this->add(self::VALIDATE, $name, $middleware, true);
160    }
161
162    /**
163     * Append a middleware to the build step.
164     *
165     * @param callable $middleware Middleware function to add.
166     * @param string   $name       Name of the middleware.
167     */
168    public function appendBuild(callable $middleware, $name = null)
169    {
170        $this->add(self::BUILD, $name, $middleware);
171    }
172
173    /**
174     * Prepend a middleware to the build step.
175     *
176     * @param callable $middleware Middleware function to add.
177     * @param string   $name       Name of the middleware.
178     */
179    public function prependBuild(callable $middleware, $name = null)
180    {
181        $this->add(self::BUILD, $name, $middleware, true);
182    }
183
184    /**
185     * Append a middleware to the sign step.
186     *
187     * @param callable $middleware Middleware function to add.
188     * @param string   $name       Name of the middleware.
189     */
190    public function appendSign(callable $middleware, $name = null)
191    {
192        $this->add(self::SIGN, $name, $middleware);
193    }
194
195    /**
196     * Prepend a middleware to the sign step.
197     *
198     * @param callable $middleware Middleware function to add.
199     * @param string   $name       Name of the middleware.
200     */
201    public function prependSign(callable $middleware, $name = null)
202    {
203        $this->add(self::SIGN, $name, $middleware, true);
204    }
205
206    /**
207     * Append a middleware to the attempt step.
208     *
209     * @param callable $middleware Middleware function to add.
210     * @param string   $name       Name of the middleware.
211     */
212    public function appendAttempt(callable $middleware, $name = null)
213    {
214        $this->add(self::ATTEMPT, $name, $middleware);
215    }
216
217    /**
218     * Prepend a middleware to the attempt step.
219     *
220     * @param callable $middleware Middleware function to add.
221     * @param string   $name       Name of the middleware.
222     */
223    public function prependAttempt(callable $middleware, $name = null)
224    {
225        $this->add(self::ATTEMPT, $name, $middleware, true);
226    }
227
228    /**
229     * Add a middleware before the given middleware by name.
230     *
231     * @param string|callable $findName   Add before this
232     * @param string          $withName   Optional name to give the middleware
233     * @param callable        $middleware Middleware to add.
234     */
235    public function before($findName, $withName, callable $middleware)
236    {
237        $this->splice($findName, $withName, $middleware, true);
238    }
239
240    /**
241     * Add a middleware after the given middleware by name.
242     *
243     * @param string|callable $findName   Add after this
244     * @param string          $withName   Optional name to give the middleware
245     * @param callable        $middleware Middleware to add.
246     */
247    public function after($findName, $withName, callable $middleware)
248    {
249        $this->splice($findName, $withName, $middleware, false);
250    }
251
252    /**
253     * Remove a middleware by name or by instance from the list.
254     *
255     * @param string|callable $nameOrInstance Middleware to remove.
256     */
257    public function remove($nameOrInstance)
258    {
259        if (is_callable($nameOrInstance)) {
260            $this->removeByInstance($nameOrInstance);
261        } elseif (is_string($nameOrInstance)) {
262            $this->removeByName($nameOrInstance);
263        }
264    }
265
266    /**
267     * Interpose a function between each middleware (e.g., allowing for a trace
268     * through the middleware layers).
269     *
270     * The interpose function is a function that accepts a "step" argument as a
271     * string and a "name" argument string. This function must then return a
272     * function that accepts the next handler in the list. This function must
273     * then return a function that accepts a CommandInterface and optional
274     * RequestInterface and returns a promise that is fulfilled with an
275     * Aws\ResultInterface or rejected with an Aws\Exception\AwsException
276     * object.
277     *
278     * @param callable|null $fn Pass null to remove any previously set function
279     */
280    public function interpose(callable $fn = null)
281    {
282        $this->sorted = null;
283        $this->interposeFn = $fn;
284    }
285
286    /**
287     * Compose the middleware and handler into a single callable function.
288     *
289     * @return callable
290     */
291    public function resolve()
292    {
293        if (!($prev = $this->handler)) {
294            throw new \LogicException('No handler has been specified');
295        }
296
297        if ($this->sorted === null) {
298            $this->sortMiddleware();
299        }
300
301        foreach ($this->sorted as $fn) {
302            $prev = $fn($prev);
303        }
304
305        return $prev;
306    }
307
308    public function count()
309    {
310        return count($this->steps[self::INIT])
311            + count($this->steps[self::VALIDATE])
312            + count($this->steps[self::BUILD])
313            + count($this->steps[self::SIGN])
314            + count($this->steps[self::ATTEMPT]);
315    }
316
317    /**
318     * Splices a function into the middleware list at a specific position.
319     *
320     * @param          $findName
321     * @param          $withName
322     * @param callable $middleware
323     * @param          $before
324     */
325    private function splice($findName, $withName, callable $middleware, $before)
326    {
327        if (!isset($this->named[$findName])) {
328            throw new \InvalidArgumentException("$findName not found");
329        }
330
331        $idx = $this->sorted = null;
332        $step = $this->named[$findName];
333
334        if ($withName) {
335            $this->named[$withName] = $step;
336        }
337
338        foreach ($this->steps[$step] as $i => $tuple) {
339            if ($tuple[1] === $findName) {
340                $idx = $i;
341                break;
342            }
343        }
344
345        $replacement = $before
346            ? [$this->steps[$step][$idx], [$middleware, $withName]]
347            : [[$middleware, $withName], $this->steps[$step][$idx]];
348        array_splice($this->steps[$step], $idx, 1, $replacement);
349    }
350
351    /**
352     * Provides a debug string for a given callable.
353     *
354     * @param array|callable $fn Function to write as a string.
355     *
356     * @return string
357     */
358    private function debugCallable($fn)
359    {
360        if (is_string($fn)) {
361            return "callable({$fn})";
362        }
363
364        if (is_array($fn)) {
365            $ele = is_string($fn[0]) ? $fn[0] : get_class($fn[0]);
366            return "callable(['{$ele}', '{$fn[1]}'])";
367        }
368
369        return 'callable(' . spl_object_hash($fn) . ')';
370    }
371
372    /**
373     * Sort the middleware, and interpose if needed in the sorted list.
374     */
375    private function sortMiddleware()
376    {
377        $this->sorted = [];
378
379        if (!$this->interposeFn) {
380            foreach ($this->steps as $step) {
381                foreach ($step as $fn) {
382                    $this->sorted[] = $fn[0];
383                }
384            }
385            return;
386        }
387
388        $ifn = $this->interposeFn;
389        // Interpose the interposeFn into the handler stack.
390        foreach ($this->steps as $stepName => $step) {
391            foreach ($step as $fn) {
392                $this->sorted[] = $ifn($stepName, $fn[1]);
393                $this->sorted[] = $fn[0];
394            }
395        }
396    }
397
398    private function removeByName($name)
399    {
400        if (!isset($this->named[$name])) {
401            return;
402        }
403
404        $this->sorted = null;
405        $step = $this->named[$name];
406        $this->steps[$step] = array_values(
407            array_filter(
408                $this->steps[$step],
409                function ($tuple) use ($name) {
410                    return $tuple[1] !== $name;
411                }
412            )
413        );
414    }
415
416    private function removeByInstance(callable $fn)
417    {
418        foreach ($this->steps as $k => $step) {
419            foreach ($step as $j => $tuple) {
420                if ($tuple[0] === $fn) {
421                    $this->sorted = null;
422                    unset($this->named[$this->steps[$k][$j][1]]);
423                    unset($this->steps[$k][$j]);
424                }
425            }
426        }
427    }
428
429    /**
430     * Add a middleware to a step.
431     *
432     * @param string   $step       Middleware step.
433     * @param string   $name       Middleware name.
434     * @param callable $middleware Middleware function to add.
435     * @param bool     $prepend    Prepend instead of append.
436     */
437    private function add($step, $name, callable $middleware, $prepend = false)
438    {
439        $this->sorted = null;
440
441        if ($prepend) {
442            $this->steps[$step][] = [$middleware, $name];
443        } else {
444            array_unshift($this->steps[$step], [$middleware, $name]);
445        }
446
447        if ($name) {
448            $this->named[$name] = $step;
449        }
450    }
451}
452