Router.php 정보
Router.php본문
어느 정도 코드 정리가 된것 같아 공유합니다.
api 전체코드는 주말에 플러그인 게시판에 업로드 될것 같습니다.
<?php
/**
* @author Bram(us) Van Damme <*** 개인정보보호를 위한 이메일주소 노출방지 ***>
* @copyright Copyright (c), 2013 Bram(us) Van Damme
* @license MIT public license
*/
/**
* Class Router.
*/
class Router
{
/**
* @var array The route patterns and their handling functions
*/
private $afterRoutes = array();
/**
* @var array The before middleware route patterns and their handling functions
*/
private $beforeRoutes = array();
/**
* @var object|callable The function to be executed when no route has been matched
*/
protected $notFoundCallback;
/**
* @var string Current base route, used for (sub)route mounting
*/
private $baseRoute = '';
/**
* @var string The Request Method that needs to be handled
*/
private $requestedMethod = '';
/**
* @var string The Server Base Path for Router Execution
*/
private $serverBasePath;
/**
* @var string Default Controllers Namespace
*/
private $namespace = '';
private $statusCode = 200;
private $responseBody = "";
private $config = [];
public function __construct($config = null) {
if($config) {
$this->config = $config;
}
}
public function getConfig($key = null) {
if($key) {
if(array_key_exists($key, $this->config)) {
return $this->config[$key];
}
return null;
} else {
return $this->config;
}
}
public function setConfig($config) {
return $this->config = $config;
}
public function addConfig($key, $value) {
$this->config[$key] = $value;
}
public function getPdoConnection() {
return isset($this->config['pdo_db']) ? $this->config['pdo_db'] : null;
}
/**
* Store a before middleware route and a handling function to be executed when accessed using one of the specified methods.
*
* @param string $methods Allowed methods, | delimited
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function before($methods, $pattern, $fn)
{
$pattern = $this->baseRoute . '/' . trim($pattern, '/');
$pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern;
foreach (explode('|', $methods) as $method) {
$this->beforeRoutes[$method][] = array(
'pattern' => $pattern,
'fn' => $fn,
);
}
}
/**
* Store a route and a handling function to be executed when accessed using one of the specified methods.
*
* @param string $methods Allowed methods, | delimited
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function match($methods, $pattern, $fn)
{
$pattern = $this->baseRoute . '/' . trim($pattern, '/');
$pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern;
foreach (explode('|', $methods) as $method) {
$this->afterRoutes[$method][] = array(
'pattern' => $pattern,
'fn' => $fn,
);
}
}
/**
* Shorthand for a route accessed using any method.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function all($pattern, $fn)
{
$this->match('GET|POST|PUT|DELETE|OPTIONS|PATCH|HEAD', $pattern, $fn);
}
/**
* Shorthand for a route accessed using GET.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function get($pattern, $fn)
{
$this->match('GET', $pattern, $fn);
}
/**
* Shorthand for a route accessed using POST.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function post($pattern, $fn)
{
$this->match('POST', $pattern, $fn);
}
/**
* Shorthand for a route accessed using PATCH.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function patch($pattern, $fn)
{
$this->match('PATCH', $pattern, $fn);
}
/**
* Shorthand for a route accessed using DELETE.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function delete($pattern, $fn)
{
$this->match('DELETE', $pattern, $fn);
}
/**
* Shorthand for a route accessed using PUT.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function put($pattern, $fn)
{
$this->match('PUT', $pattern, $fn);
}
/**
* Shorthand for a route accessed using OPTIONS.
*
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function options($pattern, $fn)
{
$this->match('OPTIONS', $pattern, $fn);
}
/**
* Mounts a collection of callbacks onto a base route.
*
* @param string $baseRoute The route sub pattern to mount the callbacks on
* @param callable $fn The callback method
*/
public function mount($baseRoute, $fn)
{
// Track current base route
$curBaseRoute = $this->baseRoute;
// Build new base route string
$this->baseRoute .= $baseRoute;
// Call the callable
call_user_func($fn);
// Restore original base route
$this->baseRoute = $curBaseRoute;
}
/**
* Get all request headers.
*
* @return array The request headers
*/
public function getRequestHeaders()
{
$headers = array();
// If getallheaders() is available, use that
if (function_exists('getallheaders')) {
$headers = getallheaders();
// getallheaders() can return false if something went wrong
if ($headers !== false) {
return $headers;
}
}
// Method getallheaders() not available or went wrong: manually extract 'm
foreach ($_SERVER as $name => $value) {
if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) {
$headers[str_replace(array(' ', 'Http'), array('-', 'HTTP'), ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
/**
* Get the request method used, taking overrides into account.
*
* @return string The Request method to handle
*/
public function getRequestMethod()
{
// Take the method as found in $_SERVER
$method = $_SERVER['REQUEST_METHOD'];
// If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification
// @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
ob_start();
$method = 'GET';
}
// If it's a POST request, check for a method override header
elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
$headers = $this->getRequestHeaders();
if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) {
$method = $headers['X-HTTP-Method-Override'];
}
}
return $method;
}
/**
* Set a Default Lookup Namespace for Callable methods.
*
* @param string $namespace A given namespace
*/
public function setNamespace($namespace)
{
if (is_string($namespace)) {
$this->namespace = $namespace;
}
}
/**
* Get the given Namespace before.
*
* @return string The given Namespace if exists
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Execute the router: Loop all defined before middleware's and routes, and execute the handling function if a match was found.
*
* @param object|callable $callback Function to be executed after a matching route was handled (= after router middleware)
*
* @return bool
*/
public function run($callback = null)
{
// Define which method we need to handle
$this->requestedMethod = $this->getRequestMethod();
// Handle all before middlewares
if (isset($this->beforeRoutes[$this->requestedMethod])) {
$this->handle($this->beforeRoutes[$this->requestedMethod]);
}
// Handle all routes
$numHandled = 0;
if (isset($this->afterRoutes[$this->requestedMethod])) {
$numHandled = $this->handle($this->afterRoutes[$this->requestedMethod], true);
}
// If no route was handled, trigger the 404 (if any)
if ($numHandled === 0) {
$this->trigger404();
} // If a route was handled, perform the finish callback (if any)
else {
if ($callback && is_callable($callback)) {
$callback();
}
}
// If it originally was a HEAD request, clean up after ourselves by emptying the output buffer
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
ob_end_clean();
}
echo $this->responseBody;
// Return true if a route was handled, false otherwise
return $numHandled !== 0;
}
/**
* Set the 404 handling function.
*
* @param object|callable $fn The function to be executed
*/
public function set404($fn)
{
$this->notFoundCallback = $fn;
}
/**
* Triggers 404 response
*/
public function trigger404(){
if ($this->notFoundCallback) {
$this->invoke($this->notFoundCallback);
} else {
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
}
}
/**
* Handle a a set of routes: if a match is found, execute the relating handling function.
*
* @param array $routes Collection of route patterns and their handling functions
* @param bool $quitAfterRun Does the handle function need to quit after one route was matched?
*
* @return int The number of routes handled
*/
private function handle($routes, $quitAfterRun = false)
{
// Counter to keep track of the number of routes we've handled
$numHandled = 0;
// The current page URL
$uri = $this->getCurrentUri();
// Loop all routes
foreach ($routes as $route) {
// Replace all curly braces matches {} into word patterns (like Laravel)
$route['pattern'] = preg_replace('/\/{(.*?)}/', '/(.*?)', $route['pattern']);
// we have a match!
if (preg_match_all('#^' . $route['pattern'] . '$#', $uri, $matches, PREG_OFFSET_CAPTURE)) {
// Rework matches to only contain the matches, not the orig string
$matches = array_slice($matches, 1);
// Extract the matched URL parameters (and only the parameters)
$params = array_map(function ($match, $index) use ($matches) {
// We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE)
if (isset($matches[$index + 1]) && isset($matches[$index + 1][0]) && is_array($matches[$index + 1][0])) {
if ($matches[$index + 1][0][1] > -1) {
return trim(substr($match[0][0], 0, $matches[$index + 1][0][1] - $match[0][1]), '/');
}
} // We have no following parameters: return the whole lot
return isset($match[0][0]) && $match[0][1] != -1 ? trim($match[0][0], '/') : null;
}, $matches, array_keys($matches));
// Call the handling function with the URL parameters if the desired input is callable
$this->invoke($route['fn'], $params);
++$numHandled;
// If we need to quit, then quit
if ($quitAfterRun) {
break;
}
}
}
// Return the number of routes handled
return $numHandled;
}
private function invoke($fn, $params = array())
{
if (is_callable($fn)) {
try {
$returnval = call_user_func_array($fn, $params);
$this->responseBody = $returnval;
} catch (Exception $ex) {
header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
exit;
}
}
// If not, check the existence of special parameters
elseif (stripos($fn, '@') !== false) {
// Explode segments of given route
list($controller, $method) = explode('@', $fn);
// Adjust controller class if namespace has been set
if ($this->getNamespace() !== '') {
$controller = $this->getNamespace() . '\\' . $controller;
}
// Make sure it's callable
if (is_callable(array($controller, $method))) {
if ((new \ReflectionMethod($controller, $method))->isStatic()) {
forward_static_call_array(array($controller, $method), $params);
} else {
// Make sure we have an instance, to prevent "non-static method … should not be called statically" warnings
if (\is_string($controller)) {
$controller = new $controller();
}
call_user_func_array(array($controller, $method), $params);
}
}
}
}
/**
* Define the current relative URI.
*
* @return string
*/
public function getCurrentUri()
{
// Get the current Request URI and remove rewrite base path from it (= allows one to run the router in a sub folder)
$uri = substr(rawurldecode($_SERVER['REQUEST_URI']), strlen($this->getBasePath()));
// Don't take query params into account on the URL
if (strstr($uri, '?')) {
$uri = substr($uri, 0, strpos($uri, '?'));
}
// Remove trailing slash + enforce a slash at the start
return '/' . trim($uri, '/');
}
/**
* Return server base Path, and define it if isn't defined.
*
* @return string
*/
public function getBasePath()
{
// Check if server base path is defined, if not define it.
if ($this->serverBasePath === null) {
$this->serverBasePath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/';
}
return $this->serverBasePath;
}
/**
* Explicilty sets the server base path. To be used when your entry script path differs from your entry URLs.
* @see https://github.com/bramus/router/issues/82#issuecomment-466956078
*
* @param string
*/
public function setBasePath($serverBasePath)
{
$this->serverBasePath = $serverBasePath;
}
public function withStatus($code) {
$this->statusCode = $code;
switch ($code) {
case 100:
$text = 'Continue';
break;
case 101:
$text = 'Switching Protocols';
break;
case 200:
$text = 'OK';
break;
case 201:
$text = 'Created';
break;
case 202:
$text = 'Accepted';
break;
case 203:
$text = 'Non-Authoritative Information';
break;
case 204:
$text = 'No Content';
break;
case 205:
$text = 'Reset Content';
break;
case 206:
$text = 'Partial Content';
break;
case 300:
$text = 'Multiple Choices';
break;
case 301:
$text = 'Moved Permanently';
break;
case 302:
$text = 'Moved Temporarily';
break;
case 303:
$text = 'See Other';
break;
case 304:
$text = 'Not Modified';
break;
case 305:
$text = 'Use Proxy';
break;
case 400:
$text = 'Bad Request';
break;
case 401:
$text = 'Unauthorized';
break;
case 402:
$text = 'Payment Required';
break;
case 403:
$text = 'Forbidden';
break;
case 404:
$text = 'Not Found';
break;
case 405:
$text = 'Method Not Allowed';
break;
case 406:
$text = 'Not Acceptable';
break;
case 407:
$text = 'Proxy Authentication Required';
break;
case 408:
$text = 'Request Time-out';
break;
case 409:
$text = 'Conflict';
break;
case 410:
$text = 'Gone';
break;
case 411:
$text = 'Length Required';
break;
case 412:
$text = 'Precondition Failed';
break;
case 413:
$text = 'Request Entity Too Large';
break;
case 414:
$text = 'Request-URI Too Large';
break;
case 415:
$text = 'Unsupported Media Type';
break;
case 500:
$text = 'Internal Server Error';
break;
case 501:
$text = 'Not Implemented';
break;
case 502:
$text = 'Bad Gateway';
break;
case 503:
$text = 'Service Unavailable';
break;
case 504:
$text = 'Gateway Time-out';
break;
case 505:
$text = 'HTTP Version not supported';
break;
default:
$text = 'Internal Server Error';
//exit('Unknown http status code "' . htmlentities($code) . '"');
break;
}
$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
header($protocol . ' ' . $code . ' ' . $text);
}
public function withJson($result, $code = 200) {
if ($code) {
$this->withStatus($code);
}
Header('Content-Type: application/json; charset=UTF-8');
return json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
public function withBody($result, $code = 200) {
if ($code) {
$this->withStatus($code);
}
return $result;
}
}
기본 베이스는 다들 아시다시피 https://github.com/bramus/router 입니다.
단일 1개의 파일로 구성되어 있어 composer 를 사용하지 않는 장점(?)이 있습니다.
예전에 쓰던 slim 프레임웍과 닮아서 적응이 쉬웠습니다.
api 는 대부분 json 응답을 쓰기 때문에 해당 코드에 두가지 메소드 withJson, withStatus 를 추가했습니다.
이 또한 slim에서 쓰던 문법 형태가 편해 보여서 추가하였습니다.
Router 기본 코드에는 설정값을 추가하는 부분이 없어서, 설정값을 외부에서 주입할수 있게 추가하였습니다.
addConfig(), setConfig(), getConfig()
이 메소드들을 통해 그누보드 설정값과 db접속정보들을 라우터 내부에 전달합니다.
언제쯤이 될지 모르지만, 그누보드도 composer 베이스로 리뉴얼 될거라고 봅니다. 그때까지 좀 어중간하지만 이 형태의 코드를 쓰는것도 좋은 선택이 될것입니다. 그럼
!-->
추천
3
3
댓글 3개
테스트용 데모사이트가 있으면 좋을 것 같네요. 같은 데모 데이타에.. 여러개의 API를 테스트해 볼수 있게..
고생하셨습니다
그누보드도 composer 베이스로 리뉴얼 될거라고 봅니다 <- 후.. 내부에서 그런생각을 안해요 thisgun님 설득을.. ㅋㅋ