vtenext/include/RestApi/Server.php
2021-04-28 20:10:26 +02:00

1241 lines
34 KiB
PHP

<?php
/*************************************
* SPDX-FileCopyrightText: 2009-2020 Vtenext S.r.l. <info@vtenext.com>
* SPDX-License-Identifier: AGPL-3.0-only
************************************/
/* crmv@170283 */
namespace RestService;
/**
* \RestService\Server - A REST server class for RESTful APIs.
*/
class Server
{
/**
* Current routes.
*
* structure:
* array(
* '<uri>' => <callable>
* )
*
* @var array
*/
protected $routes = array();
/**
* Blacklisted http get arguments.
*
* @var array
*/
protected $blacklistedGetParameters = array('_method', '_suppress_status_code');
/**
* Current URL that triggers the controller.
*
* @var string
*/
protected $triggerUrl = '';
/**
* Contains the controller object.
*
* @var string
*/
protected $controller = '';
/**
* List of sub controllers.
*
* @var array
*/
protected $controllers = array();
/**
* Parent controller.
*
* @var \RestService\Server
*/
protected $parentController;
/**
* The client
*
* @var Client
*/
protected $client;
/**
* List of excluded methods.
*
* @var array|string array('methodOne', 'methodTwo') or * for all methods
*/
protected $collectRoutesExclude = array('__construct');
/**
* List of possible methods.
* @var array
*/
public $methods = array('get', 'post', 'put', 'delete', 'head', 'options', 'patch');
/**
* Check access function/method. Will be fired after the route has been found.
* Arguments: (url, route)
*
* @var callable
*/
protected $checkAccessFn;
/**
* Send exception function/method. Will be fired if a route-method throws a exception.
* Please die/exit in your function then.
* Arguments: (exception)
*
* @var callable
*/
protected $sendExceptionFn;
/**
* If this is true, we send file, line and backtrace if an exception has been thrown.
*
* @var boolean
*/
protected $debugMode = false;
/**
* Sets whether the service should serve route descriptions
* through the OPTIONS method.
*
* @var boolean
*/
protected $describeRoutes = true;
/**
* If this controller can not find a route,
* we fire this method and send the result.
*
* @var string
*/
protected $fallbackMethod = '';
/**
* If the lib should send HTTP status codes.
* Some Client libs does not support this, you can deactivate it via
* ->setHttpStatusCodes(false);
*
* @var boolean
*/
protected $withStatusCode = true;
/**
* @var callable
*/
protected $controllerFactory;
/**
* Constructor
*
* @param string $pTriggerUrl
* @param string|object $pControllerClass
* @param \RestService\Server $pParentController
*/
public function __construct($pTriggerUrl, $pControllerClass = null, $pParentController = null)
{
$this->normalizeUrl($pTriggerUrl);
if ($pParentController) {
$this->parentController = $pParentController;
$this->setClient($pParentController->getClient());
if ($pParentController->getCheckAccess())
$this->setCheckAccess($pParentController->getCheckAccess());
if ($pParentController->getExceptionHandler())
$this->setExceptionHandler($pParentController->getExceptionHandler());
if ($pParentController->getDebugMode())
$this->setDebugMode($pParentController->getDebugMode());
if ($pParentController->getDescribeRoutes())
$this->setDescribeRoutes($pParentController->getDescribeRoutes());
if ($pParentController->getControllerFactory())
$this->setControllerFactory($pParentController->getControllerFactory());
$this->setHttpStatusCodes($pParentController->getHttpStatusCodes());
} else {
$this->setClient(new Client($this));
}
$this->setClass($pControllerClass);
$this->setTriggerUrl($pTriggerUrl);
}
//crmv@170283
function getController() {
return $this->controller;
}
//crmv@170283e
/**
* Factory.
*
* @param string $pTriggerUrl
* @param string $pControllerClass
*
* @return Server $this
*/
public static function create($pTriggerUrl, $pControllerClass = '')
{
$clazz = get_called_class();
return new $clazz($pTriggerUrl, $pControllerClass);
}
/**
* @param callable $controllerFactory
*
* @return Server $this
*/
public function setControllerFactory(callable $controllerFactory)
{
$this->controllerFactory = $controllerFactory;
return $this;
}
/**
* @return callable
*/
public function getControllerFactory()
{
return $this->controllerFactory;
}
/**
* If the lib should send HTTP status codes.
* Some Client libs does not support it.
*
* @param boolean $pWithStatusCode
* @return Server $this
*/
public function setHttpStatusCodes($pWithStatusCode)
{
$this->withStatusCode = $pWithStatusCode;
return $this;
}
/**
*
* @return boolean
*/
public function getHttpStatusCodes()
{
return $this->withStatusCode;
}
/**
* Set the check access function/method.
* Will fired with arguments: (url, route)
*
* @param callable $pFn
* @return Server $this
*/
public function setCheckAccess($pFn)
{
$this->checkAccessFn = $pFn;
return $this;
}
/**
* Getter for checkAccess
* @return callable
*/
public function getCheckAccess()
{
return $this->checkAccessFn;
}
/**
* If this controller can not find a route,
* we fire this method and send the result.
*
* @param string $pFn Methodname of current attached class
* @return Server $this
*/
public function setFallbackMethod($pFn)
{
$this->fallbackMethod = $pFn;
return $this;
}
/**
* Getter for fallbackMethod
* @return string
*/
public function fallbackMethod()
{
return $this->fallbackMethod;
}
/**
* Sets whether the service should serve route descriptions
* through the OPTIONS method.
*
* @param boolean $pDescribeRoutes
* @return Server $this
*/
public function setDescribeRoutes($pDescribeRoutes)
{
$this->describeRoutes = $pDescribeRoutes;
return $this;
}
/**
* Getter for describeRoutes.
*
* @return boolean
*/
public function getDescribeRoutes()
{
return $this->describeRoutes;
}
/**
* Send exception function/method. Will be fired if a route-method throws a exception.
* Please die/exit in your function then.
* Arguments: (exception)
*
* @param callable $pFn
* @return Server $this
*/
public function setExceptionHandler($pFn)
{
$this->sendExceptionFn = $pFn;
return $this;
}
/**
* Getter for checkAccess
* @return callable
*/
public function getExceptionHandler()
{
return $this->sendExceptionFn;
}
/**
* If this is true, we send file, line and backtrace if an exception has been thrown.
*
* @param boolean $pDebugMode
* @return Server $this
*/
public function setDebugMode($pDebugMode)
{
$this->debugMode = $pDebugMode;
return $this;
}
/**
* Getter for checkAccess
* @return boolean
*/
public function getDebugMode()
{
return $this->debugMode;
}
/**
* Alias for getParentController()
*
* @return Server
*/
public function done()
{
return $this->getParentController();
}
/**
* Returns the parent controller
*
* @return Server $this
*/
public function getParentController()
{
return $this->parentController;
}
/**
* Set the URL that triggers the controller.
*
* @param $pTriggerUrl
* @return Server
*/
public function setTriggerUrl($pTriggerUrl)
{
$this->triggerUrl = $pTriggerUrl;
return $this;
}
/**
* Gets the current trigger url.
*
* @return string
*/
public function getTriggerUrl()
{
return $this->triggerUrl;
}
/**
* Sets the client.
*
* @param Client|string $pClient
* @return Server $this
*/
public function setClient($pClient)
{
if (is_string($pClient)) {
$pClient = new $pClient($this);
}
$this->client = $pClient;
$this->client->setupFormats();
return $this;
}
/**
* Get the current client.
*
* @return Client
*/
public function getClient()
{
return $this->client;
}
/**
* Sends a 'Bad Request' response to the client.
*
* @param $pCode
* @param $pMessage
* @throws \Exception
* @return string
*/
public function sendBadRequest($pCode, $pMessage)
{
if (is_object($pMessage) && $pMessage->xdebug_message) $pMessage = $pMessage->xdebug_message;
$msg = array('error' => $pCode, 'message' => $pMessage);
if (!$this->getClient()) throw new \Exception('client_not_found_in_ServerController');
return $this->getClient()->sendResponse('400', $msg);
}
/**
* Sends a 'Internal Server Error' response to the client.
* @param $pCode
* @param $pMessage
* @throws \Exception
* @return string
*/
public function sendError($pCode, $pMessage)
{
if (is_object($pMessage) && $pMessage->xdebug_message) $pMessage = $pMessage->xdebug_message;
$msg = array('error' => $pCode, 'message' => $pMessage);
if (!$this->getClient()) throw new \Exception('client_not_found_in_ServerController');
return $this->getClient()->sendResponse('500', $msg);
}
/**
* Sends a exception response to the client.
* @param $pException
* @throws \Exception
*/
public function sendException($pException)
{
if ($this->sendExceptionFn) {
call_user_func_array($this->sendExceptionFn, array($pException));
}
$message = $pException->getMessage();
if (is_object($message) && $message->xdebug_message) $message = $message->xdebug_message;
$msg = array('error' => get_class($pException), 'message' => $message);
if ($this->debugMode) {
$msg['file'] = $pException->getFile();
$msg['line'] = $pException->getLine();
$msg['trace'] = $pException->getTraceAsString();
}
if (!$this->getClient()) throw new \Exception('Client not found in ServerController');
return $this->getClient()->sendResponse((isset($pException->pHttpCode))?$pException->pHttpCode:'500', $msg); //crmv@170283
}
/**
* Adds a new route for all http methods (get, post, put, delete, options, head, patch).
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @param string $pHttpMethod If you want to limit to a HTTP method.
* @return Server
*/
public function addRoute($pUri, $pCb, $pHttpMethod = '_all_')
{
$this->routes[$pUri][ $pHttpMethod ] = $pCb;
return $this;
}
/**
* Same as addRoute, but limits to GET.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addGetRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'get');
return $this;
}
/**
* Same as addRoute, but limits to POST.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addPostRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'post');
return $this;
}
/**
* Same as addRoute, but limits to PUT.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addPutRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'put');
return $this;
}
/**
* Same as addRoute, but limits to PATCH.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addPatchRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'patch');
return $this;
}
/**
* Same as addRoute, but limits to HEAD.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addHeadRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'head');
return $this;
}
/**
* Same as addRoute, but limits to OPTIONS.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addOptionsRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'options');
return $this;
}
/**
* Same as addRoute, but limits to DELETE.
*
* @param string $pUri
* @param callable|string $pCb The method name of the passed controller or a php callable.
* @return Server
*/
public function addDeleteRoute($pUri, $pCb)
{
$this->addRoute($pUri, $pCb, 'delete');
return $this;
}
/**
* Removes a route.
*
* @param string $pUri
* @return Server
*/
public function removeRoute($pUri)
{
unset($this->routes[$pUri]);
return $this;
}
/**
* Sets the controller class.
*
* @param string|object $pClass
*/
public function setClass($pClass)
{
if (is_string($pClass)) {
$this->createControllerClass($pClass);
} elseif (is_object($pClass)) {
$this->controller = $pClass;
} else {
$this->controller = $this;
}
}
/**
* Setup the controller class.
*
* @param string $pClassName
* @throws \Exception
*/
protected function createControllerClass($pClassName)
{
if ($pClassName != '') {
try {
if ($this->controllerFactory) {
$this->controller = call_user_func_array($this->controllerFactory, array(
$pClassName,
$this
));
} else {
$this->controller = new $pClassName($this);
}
if (get_parent_class($this->controller) == '\RestService\Server') {
$this->controller->setClient($this->getClient());
}
} catch (\Exception $e) {
throw new \Exception('Error during initialisation of '.$pClassName.': '.$e, 0, $e);
}
} else {
$this->controller = $this;
}
}
/**
* Attach a sub controller.
*
* @param string $pTriggerUrl
* @param mixed $pControllerClass A class name (autoloader required) or a instance of a class.
*
* @return Server new created Server. Use done() to switch the context back to the parent.
*/
public function addSubController($pTriggerUrl, $pControllerClass = '')
{
$this->normalizeUrl($pTriggerUrl);
$base = $this->triggerUrl;
if ($base == '/') $base = '';
$controller = new Server($base . $pTriggerUrl, $pControllerClass, $this);
$this->controllers[] = $controller;
return $controller;
}
/**
* Normalize $pUrl. Cuts of the trailing slash.
*
* @param string $pUrl
*/
public function normalizeUrl(&$pUrl)
{
if ('/' === $pUrl) return;
if (substr($pUrl, -1) == '/') $pUrl = substr($pUrl, 0, -1);
if (substr($pUrl, 0, 1) != '/') $pUrl = '/' . $pUrl;
}
/**
* Sends data to the client with 200 http code.
*
* @param $pData
*/
public function send($pData)
{
return $this->getClient()->sendResponse(200, array('data' => $pData));
}
/**
* @param string $pValue
* @return string
*/
public function camelCase2Dashes($pValue)
{
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $pValue));
}
/**
* Setup automatic routes.
*
* @return Server
*/
public function collectRoutes()
{
if ($this->collectRoutesExclude == '*') return $this;
$methods = get_class_methods($this->controller);
foreach ($methods as $method) {
if (in_array($method, $this->collectRoutesExclude)) continue;
$info = explode('/', preg_replace('/([a-z]*)(([A-Z]+)([a-zA-Z0-9_]*))/', '$1/$2', $method));
$uri = $this->camelCase2Dashes((empty($info[1]) ? '' : $info[1]));
$httpMethod = $info[0];
if ($httpMethod == 'all') {
$httpMethod = '_all_';
}
$reflectionMethod = new \ReflectionMethod($this->controller, $method);
if ($reflectionMethod->isPrivate()) continue;
$phpDocs = $this->getMethodMetaData($reflectionMethod);
if (isset($phpDocs['url'])) {
if (isset($phpDocs['url']['url'])) {
//only one route
$this->routes[$phpDocs['url']['url']][$httpMethod] = $method;
} else {
foreach($phpDocs['url'] as $urlAnnotation) {
$this->routes[$urlAnnotation['url']][$httpMethod] = $method;
}
}
} else {
$this->routes[$uri][$httpMethod] = $method;
}
}
return $this;
}
/**
* Simulates a HTTP Call.
*
* @param string $pUri
* @param string $pMethod The HTTP Method
* @return string
*/
public function simulateCall($pUri, $pMethod = 'get')
{
if (($idx = strpos($pUri, '?')) !== false) {
parse_str(substr($pUri, $idx+1), $_GET);
$pUri = substr($pUri, 0, $idx);
}
$this->getClient()->setUrl($pUri);
$this->getClient()->setMethod($pMethod);
return $this->run();
}
/**
* Fire the magic!
*
* Searches the method and sends the data to the client.
*
* @return mixed
*/
public function run()
{
//check sub controller
foreach ($this->controllers as $controller) {
if ($result = $controller->run()) {
return $result;
}
}
$requestedUrl = $this->getClient()->getUrl();
$this->normalizeUrl($requestedUrl);
//check if its in our area
if (strpos($requestedUrl, $this->triggerUrl) !== 0) return;
$endPos = $this->triggerUrl === '/' ? 1 : strlen($this->triggerUrl) + 1;
$uri = substr($requestedUrl, $endPos);
if (!$uri) $uri = '';
$route = false;
$arguments = array();
$requiredMethod = $this->getClient()->getMethod();
//does the requested uri exist?
list($callableMethod, $regexArguments, $method, $routeUri) = $this->findRoute($uri, $requiredMethod);
if ((!$callableMethod || $method != 'options') && $requiredMethod == 'options') {
$description = $this->describe($uri);
$this->send($description);
}
if (!$callableMethod) {
if (!$this->getParentController()) {
if ($this->fallbackMethod) {
$m = $this->fallbackMethod;
$this->send($this->controller->$m());
} else {
return $this->sendBadRequest('RouteNotFoundException', "There is no route for '$uri'.");
}
} else {
return false;
}
}
if ($method == '_all_')
$arguments[] = $method;
if (is_array($regexArguments)) {
$arguments = array_merge($arguments, $regexArguments);
}
//open class and scan method
//crmv@170283
/*
if ($this->controller && is_string($callableMethod)) {
$ref = new \ReflectionClass($this->controller);
if (!method_exists($this->controller, $callableMethod)) {
$this->sendBadRequest('MethodNotFoundException', "There is no method '$callableMethod' in ".
get_class($this->controller).".");
}
$reflectionMethod = $ref->getMethod($callableMethod);
} else if (is_callable($callableMethod)) {
$reflectionMethod = new \ReflectionFunction($callableMethod);
}
$params = $reflectionMethod->getParameters();
*/
$params = $this->controller->getParameters($callableMethod);
//crmv@170283e
if ($method == '_all_') {
//first parameter is $pMethod
array_shift($params);
}
//remove regex arguments
if (is_array($regexArguments)) { // crmv@181748
for ($i=0; $i<count($regexArguments); $i++) {
array_shift($params);
}
} // crmv@181748
//collect arguments
foreach ($params as $param) {
//crmv@170283
//$name = $this->argumentName($param->getName());
$name = $param;
//crmv@170283e
if ($name == '_') {
$thisArgs = array();
foreach ($_GET as $k => $v) {
if (substr($k, 0, 1) == '_' && $k != '_suppress_status_code')
$thisArgs[$k] = $v;
}
$arguments[] = $thisArgs;
} else {
//crmv@170283
/*
if (!$param->isOptional() && !isset($_GET[$name]) && !isset($_POST[$name])) {
return $this->sendBadRequest('MissingRequiredArgumentException', sprintf("Argument '%s' is missing.", $name));
}
$arguments[] = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : $param->getDefaultValue());
*/
$arguments[] = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : '');
//crmv@170283e
}
}
if ($this->checkAccessFn) {
$args[] = $this->getClient()->getUrl();
$args[] = $route;
$args[] = $arguments;
try {
call_user_func_array($this->checkAccessFn, $args);
} catch (\Exception $e) {
$this->sendException($e);
}
}
//fire method
$object = $this->controller;
return $this->fireMethod($callableMethod, $object, $arguments);
}
public function fireMethod($pMethod, $pController, $pArguments)
{
$callable = false;
if ($pController && is_string($pMethod)) {
//crmv@170283
/*
if (!method_exists($pController, $pMethod)) {
return $this->sendError('MethodNotFoundException', sprintf('Method %s in class %s not found.', $pMethod, get_class($pController)));
} else {
*/
$callable = array($pController, $pMethod);
//}
//crmv@170283e
} elseif (is_callable($pMethod)) {
$callable = $pMethod;
}
if ($callable) {
try {
return $this->send(call_user_func_array($callable, $pArguments));
} catch (\Exception $e) {
return $this->sendException($e);
}
}
}
/**
* Describe a route or the whole controller with all routes.
*
* @param string $pUri
* @param boolean $pOnlyRoutes
* @return array
*/
public function describe($pUri = null, $pOnlyRoutes = false)
{
$definition = array();
if (!$pOnlyRoutes) {
$definition['parameters'] = array(
'_method' => array('description' => 'Can be used as HTTP METHOD if the client does not support HTTP methods.', 'type' => 'string',
'values' => 'GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH'),
'_suppress_status_code' => array('description' => 'Suppress the HTTP status code.', 'type' => 'boolean', 'values' => '1, 0'),
'_format' => array('description' => 'Format of generated data. Can be added as suffix .json .xml', 'type' => 'string', 'values' => 'json, xml'),
);
}
$definition['controller'] = array(
'entryPoint' => $this->getTriggerUrl()
);
foreach ($this->routes as $routeUri => $routeMethods) {
$matches = array();
if (!$pUri || ($pUri && preg_match('|^'.$routeUri.'$|', $pUri, $matches))) {
if ($matches) {
array_shift($matches);
}
$def = array();
$def['uri'] = $this->getTriggerUrl().'/'.$routeUri;
foreach ($routeMethods as $method => $phpMethod) {
if (is_string($phpMethod)) {
$ref = new \ReflectionClass($this->controller);
$refMethod = $ref->getMethod($phpMethod);
} else {
$refMethod = new \ReflectionFunction($phpMethod);
}
$def['methods'][strtoupper($method)] = $this->getMethodMetaData($refMethod, $matches);
}
$definition['controller']['routes'][$routeUri] = $def;
}
}
if (!$pUri) {
foreach ($this->controllers as $controller) {
$definition['subController'][$controller->getTriggerUrl()] = $controller->describe(false, true);
}
}
return $definition;
}
/**
* Fetches all meta data informations as params, return type etc.
*
* @param \ReflectionMethod $pMethod
* @param array $pRegMatches
* @return array
*/
public function getMethodMetaData(\ReflectionFunctionAbstract $pMethod, $pRegMatches = null)
{
$file = $pMethod->getFileName();
$startLine = $pMethod->getStartLine();
$fh = fopen($file, 'r');
if (!$fh) return false;
$lineNr = 1;
$lines = array();
while (($buffer = fgets($fh)) !== false) {
if ($lineNr == $startLine) break;
$lines[$lineNr] = $buffer;
$lineNr++;
}
fclose($fh);
$phpDoc = '';
$blockStarted = false;
while ($line = array_pop($lines)) {
if ($blockStarted) {
$phpDoc = $line.$phpDoc;
//if start comment block: /*
if (preg_match('/\s*\t*\/\*/', $line)) {
break;
}
continue;
} else {
//we are not in a comment block.
//if class def, array def or close bracked from fn comes above
//then we dont have phpdoc
if (preg_match('/^\s*\t*[a-zA-Z_&\s]*(\$|{|})/', $line)) {
break;
}
}
$trimmed = trim($line);
if ($trimmed == '') continue;
//if end comment block: */
if (preg_match('/\*\//', $line)) {
$phpDoc = $line.$phpDoc;
$blockStarted = true;
//one line php doc?
if (preg_match('/\s*\t*\/\*/', $line)) {
break;
}
}
}
$phpDoc = $this->parsePhpDoc($phpDoc);
$refParams = $pMethod->getParameters();
$params = array();
$fillPhpDocParam = !isset($phpDoc['param']);
foreach ($refParams as $param) {
$params[$param->getName()] = $param;
if ($fillPhpDocParam) {
$phpDoc['param'][] = array(
'name' => $param->getName(),
'type' => $param->isArray()?'array':'mixed'
);
}
}
$parameters = array();
if (isset($phpDoc['param'])) {
if (is_array($phpDoc['param']) && is_string(key($phpDoc['param'])))
$phpDoc['param'] = array($phpDoc['param']);
$c = 0;
foreach ($phpDoc['param'] as $phpDocParam) {
$param = $params[$phpDocParam['name']];
if (!$param) continue;
$parameter = array(
'type' => $phpDocParam['type']
);
if ($pRegMatches && is_array($pRegMatches) && $pRegMatches[$c]) {
$parameter['fromRegex'] = '$'.($c+1);
}
$parameter['required'] = !$param->isOptional();
if ($param->isDefaultValueAvailable()) {
$parameter['default'] = str_replace(array("\n", ' '), '', var_export($param->getDefaultValue(), true));
}
$parameters[$this->argumentName($phpDocParam['name'])] = $parameter;
$c++;
}
}
if (!isset($phpDoc['return']))
$phpDoc['return'] = array('type' => 'mixed');
$result = array(
'parameters' => $parameters,
'return' => $phpDoc['return']
);
if (isset($phpDoc['description']))
$result['description'] = $phpDoc['description'];
if (isset($phpDoc['url']))
$result['url'] = $phpDoc['url'];
return $result;
}
/**
* Parse phpDoc string and returns an array.
*
* @param string $pString
* @return array
*/
public function parsePhpDoc($pString)
{
preg_match('#^/\*\*(.*)\*/#s', trim($pString), $comment);
if (0 === count($comment)) return array();
$comment = trim($comment[1]);
preg_match_all('/^\s*\*(.*)/m', $comment, $lines);
$lines = $lines[1];
$tags = array();
$currentTag = '';
$currentData = '';
foreach ($lines as $line) {
$line = trim($line);
if (substr($line, 0, 1) == '@') {
if ($currentTag)
$tags[$currentTag][] = $currentData;
else
$tags['description'] = $currentData;
$currentData = '';
preg_match('/@([a-zA-Z_]*)/', $line, $match);
$currentTag = $match[1];
}
$currentData = trim($currentData.' '.$line);
}
if ($currentTag)
$tags[$currentTag][] = $currentData;
else
$tags['description'] = $currentData;
//parse tags
$regex = array(
'param' => array('/^@param\s*\t*([a-zA-Z_\\\[\]]*)\s*\t*\$([a-zA-Z_]*)\s*\t*(.*)/', array('type', 'name', 'description')),
'url' => array('/^@url\s*\t*(.+)/', array('url')),
'return' => array('/^@return\s*\t*([a-zA-Z_\\\[\]]*)\s*\t*(.*)/', array('type', 'description')),
);
foreach ($tags as $tag => &$data) {
if ($tag == 'description') continue;
foreach ($data as &$item) {
if (isset($regex[$tag])) {
preg_match($regex[$tag][0], $item, $match);
$item = array();
$c = count($match);
for ($i =1; $i < $c; $i++) {
if (isset($regex[$tag][1][$i-1])) {
$item[$regex[$tag][1][$i-1]] = $match[$i];
}
}
}
}
if (count($data) == 1)
$data = $data[0];
}
return $tags;
}
/**
* If the name is a camelcased one whereas the first char is lowercased,
* then we remove the first char and set first char to lower case.
*
* @param string $pName
* @return string
*/
public function argumentName($pName)
{
if (ctype_lower(substr($pName, 0, 1)) && ctype_upper(substr($pName, 1, 1))) {
return strtolower(substr($pName, 1, 1)).substr($pName, 2);
} return $pName;
}
/**
* Find and return the route for $pUri.
*
* @param string $pUri
* @param string $pMethod limit to method.
* @return array|boolean
*/
public function findRoute($pUri, $pMethod = '_all_')
{
if (isset($this->routes[$pUri][$pMethod]) && $method = $this->routes[$pUri][$pMethod]) {
return array($method, null, $pMethod, $pUri);
} elseif ($pMethod != '_all_' && isset($this->routes[$pUri]['_all_']) && $method = $this->routes[$pUri]['_all_']) {
return array($method, null, $pMethod, $pUri);
} else {
//maybe we have a regex uri
foreach ($this->routes as $routeUri => $routeMethods) {
if (preg_match('|^'.$routeUri.'$|', $pUri, $matches)) {
if (!isset($routeMethods[$pMethod])) {
if (isset($routeMethods['_all_']))
$pMethod = '_all_';
else
continue;
}
array_shift($matches);
foreach ($matches as $match) {
$arguments[] = $match;
}
return array($routeMethods[$pMethod], $arguments, $pMethod, $routeUri);
}
}
}
return false;
}
}