/** * Setup our test * (runs before each test) * * @access protected * @return void */ protected function setUp() { // Create a new klein app, // since we need one pretty much everywhere $this->router = new Router(new App()); $this->var_dir = App::i()->var_dir(); if (!file_exists($this->var_dir)) { mkdir($this->var_dir, 0777); } }
/** * @expectedException BadMethodCallException */ public function testCallBadMethod() { $app = new App(); $app->random_thing_that_doesnt_exist(); }
public function testRegisterDuplicateMethod() { $app = new App(); $app->foo(function () { return 'foo'; }); $app->foo(function () { return 'foo2'; }); }
/** * 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(); } }