public function testUniversalParams() { // Test data $params_get = array('page' => 2, 'per_page' => 10, 'num' => 1, 5 => 'ok', 'empty' => null, 'blank' => ''); $params_post = array('first_name' => 'Trevor', 'last_name' => 'Suarez', 'num' => 2, 3 => 'hmm', 4 => 'thing'); $cookies = array('user' => 'Rican7', 'PHPSESSID' => 'randomstring', 'num' => 3, 4 => 'dog'); $named = array('id' => '1f8ae', 'num' => 4); // Create the request $request = new Request($params_get, $params_post, $cookies); // Set our named params $request->paramsNamed()->replace($named); // Merge our params for our expected results $params = array_merge($params_get, $params_post, $cookies, $named); $this->assertSame($params, $request->params()); $this->assertSame($params['num'], $request->param('num')); $this->assertSame(null, $request->param('thisdoesntexist')); }
/** * Dispatch the request to the approriate 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 Response $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 * @access public * @return void|string */ public function dispatch(Request $request = null, Response $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(); // Bind our objects to our service $this->service->bind($this->request, $this->response); // Grab some data from the request $uri = $this->request->uri(true); // Strip the query string $req_method = $this->request->method(); // Set up some variables for matching $matched = 0; $methods_matched = array(); $params = array(); $apc = function_exists('apc_fetch'); ob_start(); foreach ($this->routes as $handler) { list($method, $_route, $callback, $count_match) = $handler; // 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 = is_null($method_match) || $method_match; // ! is used to negate a match if (isset($_route[0]) && $_route[0] === '!') { $negate = true; $i = 1; } else { $negate = false; $i = 0; } // Check for a wildcard (match all) if ($_route === '*') { $match = true; } elseif ($_route === '404' && !$matched && count($methods_matched) <= 0) { // Easily handle 404's try { $this->response->append(call_user_func($callback, $this->request, $this->response, $this->service, $this->app, $matched, $methods_matched)); } catch (LockedResponseException $e) { // Do nothing, since this is an automated behavior } catch (Exception $e) { $this->error($e); } ++$matched; continue; } elseif ($_route === '405' && !$matched && count($methods_matched) > 0) { // Easily handle 405's try { $this->response->append(call_user_func($callback, $this->request, $this->response, $this->service, $this->app, $matched, $methods_matched)); } catch (LockedResponseException $e) { // Do nothing, since this is an automated behavior } catch (Exception $e) { $this->error($e); } ++$matched; continue; } elseif (isset($_route[$i]) && $_route[$i] === '@') { // @ is used to specify custom regex $match = preg_match('`' . substr($_route, $i + 1) . '`', $uri, $params); } else { // Compiling and matching regular expressions is relatively // expensive, so try and match by a substring first $route = null; $regex = false; $j = 0; $n = isset($_route[$i]) ? $_route[$i] : null; // Find the longest non-regex substring and match it against the URI while (true) { if (!isset($_route[$i])) { break; } elseif (false === $regex) { $c = $n; $regex = $c === '[' || $c === '(' || $c === '.'; if (false === $regex && false !== isset($_route[$i + 1])) { $n = $_route[$i + 1]; $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{'; } if (false === $regex && $c !== '/' && (!isset($uri[$j]) || $c !== $uri[$j])) { continue 2; } $j++; } $route .= $_route[$i++]; } // Check if there's a cached regex string if (false !== $apc) { $regex = apc_fetch("route:{$route}"); if (false === $regex) { $regex = $this->compileRoute($route); apc_store("route:{$route}", $regex); } } else { $regex = $this->compileRoute($route); } $match = preg_match($regex, $uri, $params); } if (isset($match) && $match ^ $negate) { // 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); if ($possible_match) { if (!empty($params)) { $this->request->paramsNamed()->merge($params); } // Try and call our route's callback try { $this->response->append(call_user_func($callback, $this->request, $this->response, $this->service, $this->app, $matched, $methods_matched)); } catch (LockedResponseException $e) { // Do nothing, since this is an automated behavior } catch (Exception $e) { $this->error($e); } if ($_route !== '*') { $count_match && ++$matched; } } } } try { if (!$matched && count($methods_matched) > 0) { if (strcasecmp($req_method, 'OPTIONS') !== 0) { $this->response->code(405); } $this->response->header('Allow', implode(', ', $methods_matched)); } elseif (!$matched) { $this->response->code(404); } if ($this->response->chunked) { $this->response->chunk(); } else { // Output capturing behavior switch ($capture) { case self::DISPATCH_CAPTURE_AND_RETURN: return ob_get_clean(); break; case self::DISPATCH_CAPTURE_AND_REPLACE: $this->response->body(ob_get_clean()); break; case self::DISPATCH_CAPTURE_AND_PREPEND: $this->response->prepend(ob_get_clean()); break; case self::DISPATCH_CAPTURE_AND_APPEND: $this->response->append(ob_get_clean()); break; case self::DISPATCH_NO_CAPTURE: default: ob_end_flush(); break; } } // Test for HEAD request (like GET) if (strcasecmp($req_method, 'HEAD') === 0) { // HEAD requests shouldn't return a body $this->response->body(''); ob_clean(); } } catch (LockedResponseException $e) { // Do nothing, since this is an automated behavior } if ($send_response && !$this->response->isSent()) { $this->response->send(); } }
/** * Dispatch the request to the approriate 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 * @access public * @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 ?: Request::createFromGlobals(); $this->response = $response = $response ?: new Response(); // Bind our objects to our service $this->app->request(function () use($request) { return $request; }); $this->app->response(function () use($response) { return $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(); 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) { // Easily handle 40x's // 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++]; } // 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); } $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->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); } } // 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 try { 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(); } } 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(); } }