<?php
/**
* @package phprouter
*/
namespace phprouter;
/**
* Class used to map methods or functions to URIs
* @package phprouter
* @see Http
* @see Response
*/
abstract class Router
{
/**
* Whether the router has been run
* @see map()
* @see run()
* @var boolean
* @access private
*/
protected static $run = false;
/**
* Routes
* First sub-array contains default routes for specific status codes,
* second sub-array contains regular routes
* @see map()
* @see run()
* @var array
* @access private
*/
protected static $routes = array();
/**
* Set a new route
* @see $run
* @see $routes
* @param string $uri URI to bind, starting with "/"
* @param callback $callback Callback function to run
* @param array|string $methods Method(s) for which the route is set
* @throws InvalidArgumentException on bad parameter
* @throws LogicException if the router has been run yet
*/
static function map( $uri, $callback, $methods = array() )
{
if ( self::$run )
{
throw new \LogicException( 'The router has been run yet' );
}
$methods = array_map( 'strtoupper', (array) $methods );
if ( $methods === array() )
{
$methods = Http::$methods;
}
if ( array_values( array_intersect( Http::$methods, $methods ) ) != $methods )
{
throw new \InvalidArgumentException( 'Invalid HTTP method' );
}
if ( $uri[0] != '/' )
{
//
// RFC 2616, sec. 9.2 states:
//
// "If the Request-URI is an asterisk ("*"), the OPTIONS request
// is intended to apply to the server in general rather than to a
// specific resource."
//
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
//
if ( $methods !== array( 'OPTIONS' ) && $uri == '*' )
{
throw new \InvalidArgumentException( 'URI should start with "/"' );
}
}
if ( preg_match( '`^/\:' . Http::$status_codes_regex . '/?$`', $uri, $match ) )
{
self::$routes[0][ intval( $match[1] ) ] = array(
'callback' => $callback,
'methods' => $methods
);
}
else
{
self::$routes[1][] = array(
'uri' => preg_replace( '`/\:(\w+)`', '/(\w+)', $uri ),
'callback' => $callback,
'methods' => $methods
);
}
}
/**
* Run the router
* @see $run
* @see $routes
* @throws LogicException if the router has been run yet
*/
static function run()
{
if ( self::$run )
{
throw new \LogicException( 'The router has been run yet' );
}
self::$run = true;
$has_route = false;
if ( isset( self::$routes[1] ) )
{
foreach ( self::$routes[1] as $route )
{
if ( in_array( Request::method(), $route['methods'] ) &&
preg_match( '`^' . $route['uri'] . '/?$`', Request::uri(), $parameters ) )
{
array_shift( $parameters ); // $parameters[0] is the whole match
$has_route = array(
'route' => $route,
'parameters' => (array) $parameters
);
}
}
}
if ( !$has_route )
{
Response::status( 404 );
// if no default route has been defined for 404 HTTP status code
if ( !isset( self::$routes[0][404] ) )
{
Response::header( 'Content-Type', 'text/plain' );
echo '404 Not Found';
}
else
{
call_user_func( self::$routes[0][404]['callback'] );
}
}
else
{
// start output buffering to catch callback output
ob_start();
// this must be the only output till ob_get_contents()
call_user_func_array( $has_route['route']['callback'], $has_route['parameters'] );
$buffer = ob_get_contents();
if ( $buffer == '' )
{
ob_end_clean(); // not sure if necessary
// [if buffer is empty and] no route has been defined for this
// HTTP status code
if ( !isset( self::$routes[0][ Response::$status_code ] ) )
{
// display a default message only if the HTTP status code
// isn't 2XX
if ( Response::$status_code < 200 || Response::$status_code >= 300 )
{
Response::header( 'Content-Type', 'text/plain' );
echo Http::$status_codes[ Response::$status_code ];
}
}
else
{
call_user_func( self::$routes[0][ Response::$status_code ]['callback'] );
}
}
else
{
ob_end_flush();
}
}
}
}
/* EOF */