Exemple #1
0
 public function testHttpExceptionStopsRouteMatching()
 {
     $this->expectOutputString('one');
     $this->klein_app->respond(function () {
         echo 'one';
         throw HttpException::createFromCode(404);
     });
     $this->klein_app->respond(function () {
         echo 'two';
     });
     $this->klein_app->dispatch(MockRequestFactory::create('/notroute'));
 }
Exemple #2
0
 /**
  * Dispatch the request to the appropriate route(s)
  *
  * Dispatch with optionally injected dependencies
  * This DI allows for easy testing, object mocking, or class extension
  *
  * @param Request $request The request object to give to each callback
  * @param AbstractResponse $response The response object to give to each callback
  * @param boolean $send_response Whether or not to "send" the response after the last route has been matched
  * @param int $capture Specify a DISPATCH_* constant to change the output capturing behavior
  * @return void|string
  */
 public function dispatch(Request $request = null, AbstractResponse $response = null, $send_response = true, $capture = self::DISPATCH_NO_CAPTURE)
 {
     // Set/Initialize our objects to be sent in each callback
     $this->request = $request ?: Request::createFromGlobals();
     $this->response = $response ?: new Response();
     // Prepare any named routes
     $this->routes->prepareNamed();
     // Grab some data from the request
     $uri = $this->request->pathname();
     $req_method = $this->request->method();
     // Set up some variables for matching
     $skip_num = 0;
     $matched = $this->routes->cloneEmpty();
     // Get a clone of the routes collection, as it may have been injected
     $methods_matched = array();
     $params = array();
     $apc = function_exists('apc_fetch');
     ob_start();
     try {
         foreach ($this->routes as $route) {
             // Are we skipping any matches?
             if ($skip_num > 0) {
                 $skip_num--;
                 continue;
             }
             // Grab the properties of the route handler
             $method = $route->getMethod();
             $path = $route->getPath();
             $count_match = $route->getCountMatch();
             // Keep track of whether this specific request method was matched
             $method_match = null;
             // Was a method specified? If so, check it against the current request method
             if (is_array($method)) {
                 foreach ($method as $test) {
                     if (strcasecmp($req_method, $test) === 0) {
                         $method_match = true;
                     } elseif (strcasecmp($req_method, 'HEAD') === 0 && (strcasecmp($test, 'HEAD') === 0 || strcasecmp($test, 'GET') === 0)) {
                         // Test for HEAD request (like GET)
                         $method_match = true;
                     }
                 }
                 if (null === $method_match) {
                     $method_match = false;
                 }
             } elseif (null !== $method && strcasecmp($req_method, $method) !== 0) {
                 $method_match = false;
                 // Test for HEAD request (like GET)
                 if (strcasecmp($req_method, 'HEAD') === 0 && (strcasecmp($method, 'HEAD') === 0 || strcasecmp($method, 'GET') === 0)) {
                     $method_match = true;
                 }
             } elseif (null !== $method && strcasecmp($req_method, $method) === 0) {
                 $method_match = true;
             }
             // If the method was matched or if it wasn't even passed (in the route callback)
             $possible_match = null === $method_match || $method_match;
             // ! is used to negate a match
             if (isset($path[0]) && $path[0] === '!') {
                 $negate = true;
                 $i = 1;
             } else {
                 $negate = false;
                 $i = 0;
             }
             // Check for a wildcard (match all)
             if ($path === '*') {
                 $match = true;
             } elseif ($path === '404' && $matched->isEmpty() && count($methods_matched) <= 0 || $path === '405' && $matched->isEmpty() && count($methods_matched) > 0) {
                 // Warn user of deprecation
                 trigger_error('Use of 404/405 "routes" is deprecated. Use $klein->onHttpError() instead.', E_USER_DEPRECATED);
                 // TODO: Possibly remove in future, here for backwards compatibility
                 $this->onHttpError($route);
                 continue;
             } elseif (isset($path[$i]) && $path[$i] === '@') {
                 // @ is used to specify custom regex
                 $match = preg_match('`' . substr($path, $i + 1) . '`', $uri, $params);
             } else {
                 // Compiling and matching regular expressions is relatively
                 // expensive, so try and match by a substring first
                 $expression = null;
                 $regex = false;
                 $j = 0;
                 $n = isset($path[$i]) ? $path[$i] : null;
                 // Find the longest non-regex substring and match it against the URI
                 while (true) {
                     if (!isset($path[$i])) {
                         break;
                     } elseif (false === $regex) {
                         $c = $n;
                         $regex = $c === '[' || $c === '(' || $c === '.';
                         if (false === $regex && false !== isset($path[$i + 1])) {
                             $n = $path[$i + 1];
                             $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
                         }
                         if (false === $regex && $c !== '/' && (!isset($uri[$j]) || $c !== $uri[$j])) {
                             continue 2;
                         }
                         $j++;
                     }
                     $expression .= $path[$i++];
                 }
                 try {
                     // Check if there's a cached regex string
                     if (false !== $apc) {
                         $regex = apc_fetch("route:{$expression}");
                         if (false === $regex) {
                             $regex = $this->compileRoute($expression);
                             apc_store("route:{$expression}", $regex);
                         }
                     } else {
                         $regex = $this->compileRoute($expression);
                     }
                 } catch (RegularExpressionCompilationException $e) {
                     throw RoutePathCompilationException::createFromRoute($route, $e);
                 }
                 $match = preg_match($regex, $uri, $params);
             }
             if (isset($match) && $match ^ $negate) {
                 if ($possible_match) {
                     if (!empty($params)) {
                         /**
                          * URL Decode the params according to RFC 3986
                          * @link http://www.faqs.org/rfcs/rfc3986
                          *
                          * Decode here AFTER matching as per @chriso's suggestion
                          * @link https://github.com/chriso/klein.php/issues/117#issuecomment-21093915
                          */
                         $params = array_map('rawurldecode', $params);
                         $this->getRequest()->setAttributes($params);
                         $this->request->paramsNamed()->merge($params);
                     }
                     // Handle our response callback
                     try {
                         $this->handleRouteCallback($route, $matched, $methods_matched);
                     } catch (DispatchHaltedException $e) {
                         switch ($e->getCode()) {
                             case DispatchHaltedException::SKIP_THIS:
                                 continue 2;
                                 break;
                             case DispatchHaltedException::SKIP_NEXT:
                                 $skip_num = $e->getNumberOfSkips();
                                 break;
                             case DispatchHaltedException::SKIP_REMAINING:
                                 break 2;
                             default:
                                 throw $e;
                         }
                     }
                     if ($path !== '*') {
                         $count_match && $matched->add($route);
                     }
                 }
                 // Don't bother counting this as a method match if the route isn't supposed to match anyway
                 if ($count_match) {
                     // Keep track of possibly matched methods
                     $methods_matched = array_merge($methods_matched, (array) $method);
                     $methods_matched = array_filter($methods_matched);
                     $methods_matched = array_unique($methods_matched);
                 }
             }
         }
         // Handle our 404/405 conditions
         if ($matched->isEmpty() && count($methods_matched) > 0) {
             // Add our methods to our allow header
             $this->response->header('Allow', implode(', ', $methods_matched));
             if (strcasecmp($req_method, 'OPTIONS') !== 0) {
                 throw HttpException::createFromCode(405);
             }
         } elseif ($matched->isEmpty()) {
             throw HttpException::createFromCode(404);
         }
     } catch (HttpExceptionInterface $e) {
         // Grab our original response lock state
         $locked = $this->response->isLocked();
         // Call our http error handlers
         $this->httpError($e, $matched, $methods_matched);
         // Make sure we return our response to its original lock state
         if (!$locked) {
             $this->response->unlock();
         }
     } catch (Exception $e) {
         $this->error($e);
     }
     try {
         if ($this->response->chunked) {
             $this->response->chunk();
         } else {
             // Output capturing behavior
             switch ($capture) {
                 case self::DISPATCH_CAPTURE_AND_RETURN:
                     $buffed_content = null;
                     if (ob_get_level()) {
                         $buffed_content = ob_get_clean();
                     }
                     return $buffed_content;
                     break;
                 case self::DISPATCH_CAPTURE_AND_REPLACE:
                     if (ob_get_level()) {
                         $this->response->body(ob_get_clean());
                     }
                     break;
                 case self::DISPATCH_CAPTURE_AND_PREPEND:
                     if (ob_get_level()) {
                         $this->response->prepend(ob_get_clean());
                     }
                     break;
                 case self::DISPATCH_CAPTURE_AND_APPEND:
                     if (ob_get_level()) {
                         $this->response->append(ob_get_clean());
                     }
                     break;
                 case self::DISPATCH_NO_CAPTURE:
                 default:
                     if (ob_get_level()) {
                         ob_end_flush();
                     }
             }
         }
         // Test for HEAD request (like GET)
         if (strcasecmp($req_method, 'HEAD') === 0) {
             // HEAD requests shouldn't return a body
             $this->response->body('');
             if (ob_get_level()) {
                 ob_clean();
             }
         }
     } catch (LockedResponseException $e) {
         // Do nothing, since this is an automated behavior
     }
     // Run our after dispatch callbacks
     $this->callAfterDispatchCallbacks();
     if ($send_response && !$this->response->isSent()) {
         $this->response->send();
     }
 }