/** * Generates cacheable url to method mapping * * @param string $className * @param string $resourcePath */ protected function generateMap($className, $resourcePath = '') { /* * Mapping Rules - Optional parameters should not be mapped to URL - if * a required parameter is of primitive type - Map them to URL - Do not * create routes with out it - if a required parameter is not primitive * type - Do not include it in URL */ $reflection = new ReflectionClass($className); $classMetadata = CommentParser::parse($reflection->getDocComment()); $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED); foreach ($methods as $method) { $methodUrl = strtolower($method->getName()); //method name should not begin with _ if ($methodUrl[0] == '_') { continue; } $doc = $method->getDocComment(); $metadata = CommentParser::parse($doc) + $classMetadata; //@access should not be private if (isset($metadata['access']) && $metadata['access'] == 'private') { continue; } $arguments = array(); $defaults = array(); $params = $method->getParameters(); $position = 0; $ignorePathTill = false; $allowAmbiguity = isset($metadata['smart-auto-routing']) && $metadata['smart-auto-routing'] != 'true' || !Defaults::$smartAutoRouting; $metadata['resourcePath'] = $resourcePath; if (isset($classMetadata['description'])) { $metadata['classDescription'] = $classMetadata['description']; } if (isset($classMetadata['classLongDescription'])) { $metadata['classLongDescription'] = $classMetadata['longDescription']; } if (!isset($metadata['param'])) { $metadata['param'] = array(); } foreach ($params as $param) { $type = $param->isArray() ? 'array' : $param->getClass(); if ($type instanceof ReflectionClass) { $type = $type->getName(); } $arguments[$param->getName()] = $position; $defaults[$position] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; if (!isset($metadata['param'][$position])) { $metadata['param'][$position] = array(); } $m =& $metadata['param'][$position]; if (isset($type)) { $m['type'] = $type; } $m['name'] = trim($param->getName(), '$ '); $m['default'] = $defaults[$position]; $m['required'] = !$param->isOptional(); if (isset($m[CommentParser::$embeddedDataName]['from'])) { $from = $m[CommentParser::$embeddedDataName]['from']; } else { if (isset($type) && Util::isObjectOrArray($type) || $param->getName() == Defaults::$fullRequestDataName) { $from = 'body'; } elseif ($m['required']) { $from = 'path'; } else { $from = 'query'; } } $m['from'] = $from; if (!$allowAmbiguity && $from == 'path') { $ignorePathTill = $position + 1; } $position++; } $accessLevel = 0; if ($method->isProtected()) { $accessLevel = 3; } elseif (isset($metadata['access'])) { if ($metadata['access'] == 'protected') { $accessLevel = 2; } elseif ($metadata['access'] == 'hybrid') { $accessLevel = 1; } } elseif (isset($metadata['protected'])) { $accessLevel = 2; } /* echo " access level $accessLevel for $className::" .$method->getName().$method->isProtected().PHP_EOL; */ // take note of the order $call = array('className' => $className, 'path' => rtrim($resourcePath, '/'), 'methodName' => $method->getName(), 'arguments' => $arguments, 'defaults' => $defaults, 'metadata' => $metadata, 'accessLevel' => $accessLevel); // if manual route if (preg_match_all('/@url\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)' . '[ \\t]*\\/?(\\S*)/s', $doc, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $httpMethod = $match[1]; $url = rtrim($resourcePath . $match[2], '/'); $this->routes[$httpMethod][$url] = $call; } //if auto route enabled, do so } elseif (Defaults::$autoRoutingEnabled) { // no configuration found so use convention if (preg_match_all('/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', $methodUrl, $matches)) { $httpMethod = strtoupper($matches[0][0]); $methodUrl = substr($methodUrl, strlen($httpMethod)); } else { $httpMethod = 'GET'; } if ($methodUrl == 'index') { $methodUrl = ''; } $url = empty($methodUrl) ? rtrim($resourcePath, '/') : $resourcePath . $methodUrl; if (!$ignorePathTill) { $this->routes[$httpMethod][$url] = $call; } $position = 1; foreach ($params as $param) { $from = $metadata['param'][$position - 1]['from']; if ($from == 'body' && ($httpMethod == 'GET' || $httpMethod == 'DELETE')) { $from = $metadata['param'][$position - 1]['from'] = 'query'; } if (!$allowAmbiguity && $from != 'path') { break; } if (!empty($url)) { $url .= '/'; } $url .= '{' . $param->getName() . '}'; if ($allowAmbiguity || $position == $ignorePathTill) { $this->routes[$httpMethod][$url] = $call; } $position++; } } } }
/** * Route the public and protected methods of an Api class * * @param string $className * @param string $resourcePath * @param int $version * * @throws RestException */ public static function addAPIClass($className, $resourcePath = '', $version = 1) { /* * Mapping Rules * ============= * * - Optional parameters should not be mapped to URL * - If a required parameter is of primitive type * - If one of the self::$prefixingParameterNames * - Map it to URL * - Else If request method is POST/PUT/PATCH * - Map it to body * - Else If request method is GET/DELETE * - Map it to body * - If a required parameter is not primitive type * - Do not include it in URL */ $class = new ReflectionClass($className); $dataName = CommentParser::$embeddedDataName; try { $classMetadata = CommentParser::parse($class->getDocComment()); } catch (Exception $e) { throw new RestException(500, "Error while parsing comments of `{$className}` class. " . $e->getMessage()); } $classMetadata['scope'] = $scope = static::scope($class); $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED); foreach ($methods as $method) { $methodUrl = strtolower($method->getName()); //method name should not begin with _ if ($methodUrl[0] == '_') { continue; } $doc = $method->getDocComment(); try { $metadata = CommentParser::parse($doc) + $classMetadata; } catch (Exception $e) { throw new RestException(500, "Error while parsing comments of `{$className}::{$method->getName()}` method. " . $e->getMessage()); } //@access should not be private if (isset($metadata['access']) && $metadata['access'] == 'private') { continue; } $arguments = array(); $defaults = array(); $params = $method->getParameters(); $position = 0; $pathParams = array(); $allowAmbiguity = isset($metadata['smart-auto-routing']) && $metadata['smart-auto-routing'] != 'true' || !Defaults::$smartAutoRouting; $metadata['resourcePath'] = trim($resourcePath, '/'); if (isset($classMetadata['description'])) { $metadata['classDescription'] = $classMetadata['description']; } if (isset($classMetadata['classLongDescription'])) { $metadata['classLongDescription'] = $classMetadata['longDescription']; } if (!isset($metadata['param'])) { $metadata['param'] = array(); } if (isset($metadata['return']['type'])) { if ($qualified = Scope::resolve($metadata['return']['type'], $scope)) { list($metadata['return']['type'], $metadata['return']['children']) = static::getTypeAndModel(new ReflectionClass($qualified), $scope); } } else { //assume return type is array $metadata['return']['type'] = 'array'; } foreach ($params as $param) { $children = array(); $type = $param->isArray() ? 'array' : $param->getClass(); $arguments[$param->getName()] = $position; $defaults[$position] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; if (!isset($metadata['param'][$position])) { $metadata['param'][$position] = array(); } $m =& $metadata['param'][$position]; $m['name'] = $param->getName(); if (!isset($m[$dataName])) { $m[$dataName] = array(); } $p =& $m[$dataName]; if (empty($m['label'])) { $m['label'] = Text::title($m['name']); } if (is_null($type) && isset($m['type'])) { $type = $m['type']; } if (isset(static::$fieldTypesByName[$m['name']]) && empty($p['type']) && $type == 'string') { $p['type'] = static::$fieldTypesByName[$m['name']]; } $m['default'] = $defaults[$position]; $m['required'] = !$param->isOptional(); $contentType = Util::nestedValue($p, 'type'); if ($type == 'array' && $contentType && ($qualified = Scope::resolve($contentType, $scope))) { list($p['type'], $children, $modelName) = static::getTypeAndModel(new ReflectionClass($qualified), $scope, $className . Text::title($methodUrl), $p); } if ($type instanceof ReflectionClass) { list($type, $children, $modelName) = static::getTypeAndModel($type, $scope, $className . Text::title($methodUrl), $p); } elseif ($type && is_string($type) && ($qualified = Scope::resolve($type, $scope))) { list($type, $children, $modelName) = static::getTypeAndModel(new ReflectionClass($qualified), $scope, $className . Text::title($methodUrl), $p); } if (isset($type)) { $m['type'] = $type; } $m['children'] = $children; if (isset($modelName)) { $m['model'] = $modelName; } if ($m['name'] == Defaults::$fullRequestDataName) { $from = 'body'; if (!isset($m['type'])) { $type = $m['type'] = 'array'; } } elseif (isset($p['from'])) { $from = $p['from']; } else { if (isset($type) && Util::isObjectOrArray($type)) { $from = 'body'; if (!isset($type)) { $type = $m['type'] = 'array'; } } elseif ($m['required'] && in_array($m['name'], static::$prefixingParameterNames)) { $from = 'path'; } else { $from = 'body'; } } $p['from'] = $from; if (!isset($m['type'])) { $type = $m['type'] = static::type($defaults[$position]); } if ($allowAmbiguity || $from == 'path') { $pathParams[] = $position; } $position++; } $accessLevel = 0; if ($method->isProtected()) { $accessLevel = 3; } elseif (isset($metadata['access'])) { if ($metadata['access'] == 'protected') { $accessLevel = 2; } elseif ($metadata['access'] == 'hybrid') { $accessLevel = 1; } } elseif (isset($metadata['protected'])) { $accessLevel = 2; } /* echo " access level $accessLevel for $className::" .$method->getName().$method->isProtected().PHP_EOL; */ // take note of the order $call = array('url' => null, 'className' => $className, 'path' => rtrim($resourcePath, '/'), 'methodName' => $method->getName(), 'arguments' => $arguments, 'defaults' => $defaults, 'metadata' => $metadata, 'accessLevel' => $accessLevel); // if manual route if (preg_match_all('/@url\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)' . '[ \\t]*\\/?(\\S*)/s', $doc, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $httpMethod = $match[1]; $url = rtrim($resourcePath . $match[2], '/'); //deep copy the call, as it may change for each @url $copy = unserialize(serialize($call)); foreach ($copy['metadata']['param'] as $i => $p) { $inPath = strpos($url, '{' . $p['name'] . '}') || strpos($url, ':' . $p['name']); if ($inPath) { $copy['metadata']['param'][$i][$dataName]['from'] = 'path'; } elseif ($httpMethod == 'GET' || $httpMethod == 'DELETE') { $copy['metadata']['param'][$i][$dataName]['from'] = 'query'; } elseif (empty($p[$dataName]['from']) || $p[$dataName]['from'] == 'path') { $copy['metadata']['param'][$i][$dataName]['from'] = 'body'; } } $url = preg_replace_callback('/{[^}]+}|:[^\\/]+/', function ($matches) use($copy) { $match = trim($matches[0], '{}:'); $index = $copy['arguments'][$match]; return '{' . Routes::typeChar(isset($copy['metadata']['param'][$index]['type']) ? $copy['metadata']['param'][$index]['type'] : null) . $index . '}'; }, $url); static::addPath($url, $copy, $httpMethod, $version); } //if auto route enabled, do so } elseif (Defaults::$autoRoutingEnabled) { // no configuration found so use convention if (preg_match_all('/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', $methodUrl, $matches)) { $httpMethod = strtoupper($matches[0][0]); $methodUrl = substr($methodUrl, strlen($httpMethod)); } else { $httpMethod = 'GET'; } if ($methodUrl == 'index') { $methodUrl = ''; } $url = empty($methodUrl) ? rtrim($resourcePath, '/') : $resourcePath . $methodUrl; $lastPathParam = array_keys($pathParams); $lastPathParam = end($lastPathParam); for ($position = 0; $position < count($params); $position++) { $from = $metadata['param'][$position][$dataName]['from']; if ($from == 'body' && ($httpMethod == 'GET' || $httpMethod == 'DELETE')) { $call['metadata']['param'][$position][$dataName]['from'] = 'query'; } } if (empty($pathParams) || $allowAmbiguity) { static::addPath($url, $call, $httpMethod, $version); } foreach ($pathParams as $position) { if (!empty($url)) { $url .= '/'; } $url .= '{' . static::typeChar(isset($call['metadata']['param'][$position]['type']) ? $call['metadata']['param'][$position]['type'] : null) . $position . '}'; if ($allowAmbiguity || $position == $lastPathParam) { static::addPath($url, $call, $httpMethod, $version); } } } } }
/** * Find the data type of the given value. * * * @param mixed $o given value for finding type * * @param bool $appendToModels if an object is found should we append to * our models list? * * @return string * * @access private */ public function getType($o, $appendToModels = false) { if (is_object($o)) { $oc = get_class($o); if ($appendToModels) { $this->_model($oc, $o); } return $this->_noNamespace($oc); } if (is_array($o)) { if (count($o)) { $child = end($o); if (Util::isObjectOrArray($child)) { $childType = $this->getType($child, $appendToModels); return "Array[{$childType}]"; } } return 'array'; } if (is_bool($o)) { return 'boolean'; } if (is_numeric($o)) { return is_float($o) ? 'float' : 'int'; } return 'string'; }