ICanBoogie/HTTP v2.4.0
  • Namespace
  • Class

Namespaces

  • ICanBoogie
    • Exception
    • HTTP
      • Dispatcher
      • Headers
      • Request

Classes

  • ICanBoogie\Exception\RescueEvent
  • ICanBoogie\HTTP\CallableDispatcher
  • ICanBoogie\HTTP\Dispatcher
  • ICanBoogie\HTTP\Dispatcher\BeforeDispatchEvent
  • ICanBoogie\HTTP\Dispatcher\DispatchEvent
  • ICanBoogie\HTTP\File
  • ICanBoogie\HTTP\FileInfo
  • ICanBoogie\HTTP\FileList
  • ICanBoogie\HTTP\Headers
  • ICanBoogie\HTTP\Headers\CacheControl
  • ICanBoogie\HTTP\Headers\ContentDisposition
  • ICanBoogie\HTTP\Headers\ContentType
  • ICanBoogie\HTTP\Headers\Date
  • ICanBoogie\HTTP\Headers\Header
  • ICanBoogie\HTTP\Headers\HeaderParameter
  • ICanBoogie\HTTP\Helpers
  • ICanBoogie\HTTP\RedirectResponse
  • ICanBoogie\HTTP\Request
  • ICanBoogie\HTTP\Request\Context
  • ICanBoogie\HTTP\Response
  • ICanBoogie\HTTP\Status
  • ICanBoogie\HTTP\WeightedDispatcher

Interfaces

  • ICanBoogie\HTTP\DispatcherInterface
  • ICanBoogie\HTTP\Exception

Exceptions

  • ICanBoogie\HTTP\DispatcherNotDefined
  • ICanBoogie\HTTP\ForceRedirect
  • ICanBoogie\HTTP\MethodNotSupported
  • ICanBoogie\HTTP\NotFound
  • ICanBoogie\HTTP\ServiceUnavailable
  • ICanBoogie\HTTP\StatusCodeNotValid
  1 <?php
  2 
  3 /*
  4  * This file is part of the ICanBoogie package.
  5  *
  6  * (c) Olivier Laviale <olivier.laviale@gmail.com>
  7  *
  8  * For the full copyright and license information, please view the LICENSE
  9  * file that was distributed with this source code.
 10  */
 11 
 12 namespace ICanBoogie\HTTP;
 13 
 14 use ICanBoogie\Exception\RescueEvent;
 15 
 16 /**
 17  * Dispatches HTTP requests.
 18  *
 19  * ## Events
 20  *
 21  * - `ICanBoogie\HTTP\Dispatcher::dispatch:before` of class {@link Dispatcher\BeforeDispatchEvent}.
 22  * - `ICanBoogie\HTTP\Dispatcher::dispatch` of class {@link Dispatcher\DispatchEvent}.
 23  * - `ICanBoogie\HTTP\Dispatcher::rescue` of class {@link ICanBoogie\Exception\RescueEvent}.
 24  */
 25 class Dispatcher implements \ArrayAccess, \IteratorAggregate, DispatcherInterface
 26 {
 27     /**
 28      * The dispatchers called during the dispatching of the request.
 29      *
 30      * @var array
 31      */
 32     protected $dispatchers = [];
 33 
 34     /**
 35      * The weights of the dispatchers.
 36      *
 37      * @var array
 38      */
 39     protected $dispatchers_weight = [];
 40 
 41     protected $dispatchers_order;
 42 
 43     /**
 44      * Initializes the {@link $dispatchers} property.
 45      *
 46      * Dispatchers can be defined as callable or class name. If a dispatcher definition is not a
 47      * callable it is used as class name to instantiate a dispatcher.
 48      *
 49      * @param array $dispatchers
 50      */
 51     public function __construct(array $dispatchers = [])
 52     {
 53         foreach ($dispatchers as $dispatcher_id => $dispatcher)
 54         {
 55             $this[$dispatcher_id] = $dispatcher;
 56         }
 57     }
 58 
 59     /**
 60      * Dispatches the request to retrieve a {@link Response}.
 61      *
 62      * The request is dispatched by the {@link dispatch()} method. If an exception is thrown
 63      * during the dispatch the {@link rescue()} method is used to rescue the exception and
 64      * retrieve a {@link Response}.
 65      *
 66      * ## HEAD requests
 67      *
 68      * If a {@link NotFound} exception is caught during the dispatching of a request with a
 69      * {@link Request::METHOD_HEAD} method the following happens:
 70      *
 71      * 1. The request is cloned and the method of the cloned request is changed to
 72      * {@link Request::METHOD_GET}.
 73      * 2. The cloned method is dispatched.
 74      * 3. If the result is *not* a {@link Response} instance, the result is returned.
 75      * 4. Otherwise, a new {@link Response} instance is created with a `null` body, but the status
 76      * code and headers of the original response.
 77      * 5. The new response is returned.
 78      *
 79      * @param Request $request
 80      *
 81      * @return Response
 82      */
 83     public function __invoke(Request $request)
 84     {
 85         $response = $this->handle($request);
 86 
 87         if ($request->is_head && $response->body)
 88         {
 89             return new Response(null, $response->status, $response->headers);
 90         }
 91 
 92         return $response;
 93     }
 94 
 95     private function handle(Request $request)
 96     {
 97         try
 98         {
 99             return $this->dispatch($request);
100         }
101         catch (\Exception $e)
102         {
103             if ($e instanceof NotFound && $request->is_head)
104             {
105                 return $this->handle_head($request);
106             }
107 
108             return $this->rescue($e, $request);
109         }
110     }
111 
112     /**
113      * Trying to rescue a NotFound HEAD request using GET instead.
114      *
115      * @param Request $request
116      *
117      * @return Response
118      */
119     private function handle_head(Request $request)
120     {
121         $response = $this->handle($request->with([ 'is_get' => true ]));
122 
123         if ($response->content_length === null)
124         {
125             try
126             {
127                 $response->content_length = strlen((string) $response->body);
128             }
129             catch (\Exception $e)
130             {
131                 #
132                 # It's not that bad if we can't obtain the length of the body.
133                 #
134             }
135         }
136 
137         return $response;
138     }
139 
140     /**
141      * Checks if the dispatcher is defined.
142      *
143      * @param string $dispatcher_id The identifier of the dispatcher.
144      *
145      * @return bool `true` if the dispatcher is defined, `false` otherwise.
146      */
147     public function offsetExists($dispatcher_id)
148     {
149         return isset($this->dispatchers[$dispatcher_id]);
150     }
151 
152     /**
153      * Returns a dispatcher.
154      *
155      * @param string $dispatcher_id The identifier of the dispatcher.
156      *
157      * @return mixed
158      */
159     public function offsetGet($dispatcher_id)
160     {
161         if (!$this->offsetExists($dispatcher_id))
162         {
163             throw new DispatcherNotDefined($dispatcher_id);
164         }
165 
166         return $this->dispatchers[$dispatcher_id];
167     }
168 
169     /**
170      * Defines a dispatcher.
171      *
172      * @param string $dispatcher_id The identifier of the dispatcher.
173      * @param mixed $dispatcher The dispatcher class or callback.
174      */
175     public function offsetSet($dispatcher_id, $dispatcher)
176     {
177         $weight = 0;
178 
179         if ($dispatcher instanceof WeightedDispatcher)
180         {
181             $weight = $dispatcher->weight;
182             $dispatcher = $dispatcher->dispatcher;
183         }
184 
185         $this->dispatchers[$dispatcher_id] = $dispatcher;
186         $this->dispatchers_weight[$dispatcher_id] = $weight;
187         $this->dispatchers_order = null;
188     }
189 
190     /**
191      * Removes a dispatcher.
192      *
193      * @param string $dispatcher_id The identifier of the dispatcher.
194      */
195     public function offsetUnset($dispatcher_id)
196     {
197         unset($this->dispatchers[$dispatcher_id]);
198     }
199 
200     public function getIterator()
201     {
202         if (!$this->dispatchers_order)
203         {
204             $weights = $this->dispatchers_weight;
205 
206             $this->dispatchers_order = \ICanBoogie\sort_by_weight($this->dispatchers, function($v, $k) use ($weights) {
207 
208                 return $weights[$k];
209 
210             });
211         }
212 
213         return new \ArrayIterator($this->dispatchers_order);
214     }
215 
216     /**
217      * Dispatches a request using the defined dispatchers.
218      *
219      * The method iterates over the defined dispatchers until one of them returns a
220      * {@link Response} instance. If an exception is throw during the dispatcher execution and
221      * the dispatcher implements the {@link DispatcherInterface} interface then its
222      * {@link DispatcherInterface::rescue} method is invoked to rescue the exception, otherwise the
223      * exception is just re-thrown.
224      *
225      * {@link Dispatcher\BeforeDispatchEvent} is fired before dispatchers are traversed. If a
226      * response is provided the dispatchers are skipped.
227      *
228      * {@link Dispatcher\DispatchEvent} is fired before the response is returned. The event is
229      * fired event if the dispatchers did'nt return a response. It's the last chance to get one.
230      *
231      * @param Request $request
232      *
233      * @return Response
234      *
235      * @throws \Exception If the dispatcher that raised an exception during dispatch doesn't implement
236      * {@link DispatcherInterface}.
237      * @throws NotFound when neither the events nor the dispatchers were able to provide
238      * a {@link Response}.
239      */
240     protected function dispatch(Request $request)
241     {
242         $response = null;
243 
244         new Dispatcher\BeforeDispatchEvent($this, $request, $response);
245 
246         if (!$response)
247         {
248             foreach ($this as $id => $dispatcher)
249             {
250                 #
251                 # If the dispatcher is not a callable then it is considered as a class name, which
252                 # is used to instantiate a dispatcher.
253                 #
254 
255                 if (!($dispatcher instanceof CallableDispatcher))
256                 {
257                     $this->dispatchers[$id] = $dispatcher = is_callable($dispatcher) ? new CallableDispatcher($dispatcher) : new $dispatcher;
258                 }
259 
260                 $response = $this->dispatch_with_dispatcher($dispatcher, $request);
261 
262                 if ($response) break;
263             }
264         }
265 
266         new Dispatcher\DispatchEvent($this, $request, $response);
267 
268         if (!$response)
269         {
270             throw new NotFound;
271         }
272 
273         return $response;
274     }
275 
276     /**
277      * Dispatches the request using a dispatcher.
278      *
279      * @param DispatcherInterface $dispatcher
280      * @param Request $request
281      *
282      * @return Response
283      *
284      * @throws \Exception
285      */
286     protected function dispatch_with_dispatcher(DispatcherInterface $dispatcher, Request $request)
287     {
288         try
289         {
290             $request->context->dispatcher = $dispatcher;
291 
292             $response = $dispatcher($request);
293         }
294         catch (\Exception $e)
295         {
296             $response = $dispatcher->rescue($e, $request);
297         }
298 
299         $request->context->dispatcher = null;
300 
301         return $response;
302     }
303 
304     /**
305      * Tries to get a {@link Response} object from an exception.
306      *
307      * {@link \ICanBoogie\Exception\RescueEvent} is fired with the exception as target.
308      * The response provided by one of the event hooks is returned. If there is no response the
309      * exception is thrown again.
310      *
311      * If a response is finally obtained, the `X-ICanBoogie-Rescued-Exception` header is added to
312      * indicate where the exception was thrown from.
313      *
314      * @param \Exception $exception The exception to rescue.
315      * @param Request $request The current request.
316      *
317      * @return Response
318      *
319      * @throws \Exception The exception is re-thrown if it could not be rescued.
320      */
321     public function rescue(\Exception $exception, Request $request)
322     {
323         /* @var $response Response */
324         $response = null;
325 
326         new RescueEvent($exception, $request, $response);
327 
328         if (!$response)
329         {
330             if ($exception instanceof ForceRedirect)
331             {
332                 return new RedirectResponse($exception->location, $exception->getCode());
333             }
334 
335             throw $exception;
336         }
337 
338         $pathname = $exception->getFile();
339         $root = $_SERVER['DOCUMENT_ROOT'];
340 
341         if ($root && strpos($pathname, $root) === 0)
342         {
343             $pathname = substr($pathname, strlen($root));
344         }
345 
346         $response->headers['X-ICanBoogie-Rescued-Exception'] = $pathname . '@' . $exception->getLine();
347 
348         return $response;
349     }
350 }
351 
ICanBoogie/HTTP v2.4.0 API documentation generated by ApiGen