diff --git a/code/RESTCollection.php b/code/RESTCollection.php index 5f91492..91d55e9 100644 --- a/code/RESTCollection.php +++ b/code/RESTCollection.php @@ -8,22 +8,28 @@ * - Collection only has one type of Item nested under it, but it can have multiple of that type of Item * - Item has many types of Nouns (Collections & Items) nested under it, but it can only have one of each type of Noun */ -trait RESTCollection { - use RESTNoun; +trait RESTCollection +{ + use RESTNoun; - abstract function getItems(); - abstract function getItem($id); + abstract public function getItems(); + abstract public function getItem($id); - function LinkFor($item) { - if ($item->parent !== $this) user_error('Tried to get link for noun that was not gotten from this collection', E_USER_ERROR); - return Controller::join_links($this->Link(), $item->getID()); - } + public function LinkFor($item) + { + if ($item->parent !== $this) { + user_error('Tried to get link for noun that was not gotten from this collection', E_USER_ERROR); + } + return Controller::join_links($this->Link(), $item->getID()); + } - protected function markAsNested($obj) { - $obj->parent = $this; - return $obj; - } + protected function markAsNested($obj) + { + $obj->parent = $this; + return $obj; + } } -class RESTCollection_Handler extends RESTNoun_Handler { -} \ No newline at end of file +class RESTCollection_Handler extends RESTNoun_Handler +{ +} diff --git a/code/RESTDocGenerator.php b/code/RESTDocGenerator.php index ee4c495..85d1800 100644 --- a/code/RESTDocGenerator.php +++ b/code/RESTDocGenerator.php @@ -3,429 +3,507 @@ /** * @package restassured */ -class RESTDocGenerator_MethodFilter { - - function __construct($classReflection) { - $this->methods = $classReflection->getMethods(); - } - - function asArray() { - $res = array(); - foreach ($this->methods as $method) $res[$method->getName()] = $method; - return $res; - } - - private $negate = false; - - function not() { - $this->negate = true; - return $this; - } - - private $method; - private $args; - - function __call($name, $arguments) { - // Store the method to eventually call and the arguments - $this->method = '_'.$name; - $this->args = $arguments; - // Call array filter, which will call _callback - $this->methods = array_filter($this->methods, array($this, '_callback')); - // Reset negate - $this->negate = false; - // And return self - return $this; - } - - protected function _callback($reflection) { - // Merge the array item with the externally passed arguments - $args = array_merge(array($reflection), $this->args); - // Call the particular filtering checker - $res = call_user_func_array(array($this, $this->method), $args); - // Return that value, taking negate into account - return $this->negate ? !$res : $res; - } - - function _isSubclassOf($reflection, $filteringClass) { - return $reflection->getDeclaringClass()->isSubclassOf($filteringClass); - } - - function _isPublic($reflection) { - return $reflection->isPublic(); - } - - function _isAbstract($reflection) { - return $reflection->isAbstract(); - } - - function _isStatic($reflection) { - return $reflection->isStatic(); - } - - function _isCallablePublicMethod($reflection) { - return $reflection->isPublic() && !$reflection->isAbstract() && !$reflection->isStatic(); - } - - function _startsWith($reflection, $startsWithString) { - return strpos($reflection->getName(), $startsWithString) === 0; - } - - function _nameInArray($reflection, $filteringArray) { - if (!$filteringArray) return false; - return in_array($reflection->getName(), $filteringArray); - } - - function _hasNumberOfParameters($reflection, $noOfParams) { - return $reflection->getNumberOfParameters() == $noOfParams; - } +class RESTDocGenerator_MethodFilter +{ + + public function __construct($classReflection) + { + $this->methods = $classReflection->getMethods(); + } + + public function asArray() + { + $res = array(); + foreach ($this->methods as $method) { + $res[$method->getName()] = $method; + } + return $res; + } + + private $negate = false; + + public function not() + { + $this->negate = true; + return $this; + } + + private $method; + private $args; + + public function __call($name, $arguments) + { + // Store the method to eventually call and the arguments + $this->method = '_'.$name; + $this->args = $arguments; + // Call array filter, which will call _callback + $this->methods = array_filter($this->methods, array($this, '_callback')); + // Reset negate + $this->negate = false; + // And return self + return $this; + } + + protected function _callback($reflection) + { + // Merge the array item with the externally passed arguments + $args = array_merge(array($reflection), $this->args); + // Call the particular filtering checker + $res = call_user_func_array(array($this, $this->method), $args); + // Return that value, taking negate into account + return $this->negate ? !$res : $res; + } + + public function _isSubclassOf($reflection, $filteringClass) + { + return $reflection->getDeclaringClass()->isSubclassOf($filteringClass); + } + + public function _isPublic($reflection) + { + return $reflection->isPublic(); + } + + public function _isAbstract($reflection) + { + return $reflection->isAbstract(); + } + + public function _isStatic($reflection) + { + return $reflection->isStatic(); + } + + public function _isCallablePublicMethod($reflection) + { + return $reflection->isPublic() && !$reflection->isAbstract() && !$reflection->isStatic(); + } + + public function _startsWith($reflection, $startsWithString) + { + return strpos($reflection->getName(), $startsWithString) === 0; + } + + public function _nameInArray($reflection, $filteringArray) + { + if (!$filteringArray) { + return false; + } + return in_array($reflection->getName(), $filteringArray); + } + + public function _hasNumberOfParameters($reflection, $noOfParams) + { + return $reflection->getNumberOfParameters() == $noOfParams; + } } /** * @package restassured */ -class RESTDocGenerator_ActionInspector extends ViewableData { - - function __construct($nounInspector, $handlerInspector, $name, $reflection) { - $this->nounInspector = $nounInspector; - $this->handlerInspector = $handlerInspector; - $this->name = $name; - $this->reflection = $reflection; - - $this->block = RESTDocblockParser::parse($reflection); - - parent::__construct(); - } - - function getName() { - return $this->name; - } - - function getDescription() { - return $this->block['body']; - } - - function getData() { - if (!isset($this->block['data'])) return; - - $request = $this->block['data']; - - $type = (string)$request['as']['details']; - if (!$type) $type = $this->nounInspector->noun; - - return new ArrayData(array( - 'Type' => $type, - 'Fields' => (string)$request['fields']['details'], - 'Body' => (string)$request['body'] - )); - } - - function getResponse() { - $response = $this->block['responds-with']; - - $type = (string)$response['as']['details']; - if (!$type) $type = $this->nounInspector->noun; - - return new ArrayData(array( - 'Code' => (string)$response['details'], - 'Type' => $type, - 'Fields' => (string)$response['fields']['details'], - 'Body' => (string)$response['body'] - )); - } - - function getErrorResponses() { - $res = new ArrayList(); - - if ($this->block['responds-with-error']) foreach ($this->block['responds-with-error'] as $response) { - $res->push(new ArrayData(array( - 'Code' => (string)$response['details'], - 'Body' => (string)$response['body'], - 'Type' => (string)$response['as']['details'], - 'Fields' => (string)$response['fields']['details'] - ))); - } - - return $res; - } - - function getReturnedTypes() { - $res = array(); - - $type = (string)$this->block['data']['as']['details']; - if ($type) $res[$type] = new RESTDocGenerator_NestingInspector($type); - - $type = (string)$this->block['responds-with']['as']['details']; - if ($type) $res[$type] = new RESTDocGenerator_NestingInspector($type); - - if ($this->block['responds-with-error']) foreach ($this->block['responds-with-error'] as $response) { - $type = (string)$response['as']['details']; - if ($type) $res[] = new RESTDocGenerator_NestingInspector($type); - } - - return $res; - } +class RESTDocGenerator_ActionInspector extends ViewableData +{ + + public function __construct($nounInspector, $handlerInspector, $name, $reflection) + { + $this->nounInspector = $nounInspector; + $this->handlerInspector = $handlerInspector; + $this->name = $name; + $this->reflection = $reflection; + + $this->block = RESTDocblockParser::parse($reflection); + + parent::__construct(); + } + + public function getName() + { + return $this->name; + } + + public function getDescription() + { + return $this->block['body']; + } + + public function getData() + { + if (!isset($this->block['data'])) { + return; + } + + $request = $this->block['data']; + + $type = (string)$request['as']['details']; + if (!$type) { + $type = $this->nounInspector->noun; + } + + return new ArrayData(array( + 'Type' => $type, + 'Fields' => (string)$request['fields']['details'], + 'Body' => (string)$request['body'] + )); + } + + public function getResponse() + { + $response = $this->block['responds-with']; + + $type = (string)$response['as']['details']; + if (!$type) { + $type = $this->nounInspector->noun; + } + + return new ArrayData(array( + 'Code' => (string)$response['details'], + 'Type' => $type, + 'Fields' => (string)$response['fields']['details'], + 'Body' => (string)$response['body'] + )); + } + + public function getErrorResponses() + { + $res = new ArrayList(); + + if ($this->block['responds-with-error']) { + foreach ($this->block['responds-with-error'] as $response) { + $res->push(new ArrayData(array( + 'Code' => (string)$response['details'], + 'Body' => (string)$response['body'], + 'Type' => (string)$response['as']['details'], + 'Fields' => (string)$response['fields']['details'] + ))); + } + } + + return $res; + } + + public function getReturnedTypes() + { + $res = array(); + + $type = (string)$this->block['data']['as']['details']; + if ($type) { + $res[$type] = new RESTDocGenerator_NestingInspector($type); + } + + $type = (string)$this->block['responds-with']['as']['details']; + if ($type) { + $res[$type] = new RESTDocGenerator_NestingInspector($type); + } + + if ($this->block['responds-with-error']) { + foreach ($this->block['responds-with-error'] as $response) { + $type = (string)$response['as']['details']; + if ($type) { + $res[] = new RESTDocGenerator_NestingInspector($type); + } + } + } + + return $res; + } } /** * @package restassured */ -class RESTDocGenerator_HandlerInspector extends ViewableData { - - function __construct($nounInspector, $handler) { - $this->nounInspector = $nounInspector; - - $this->handler = $handler; - $this->reflection = new ReflectionClass($handler); - - $this->actionBlocks = array(); - foreach ($this->getActionMethodReflections() as $name => $reflection) { - $this->actionBlocks[$name] = RESTDocblockParser::parse($reflection); - } - - parent::__construct(); - } - - protected function getActionMethodReflections() { - $allow = Object::get_static($this->handler, 'allowed_actions'); - $res = new RESTDocGenerator_MethodFilter($this->reflection); - - return $res->isSubclassOf('RESTNoun_Handler')->isCallablePublicMethod()->nameInArray($allow)->asArray(); - } - - function getActions() { - $res = new ArrayList(); - foreach ($this->getActionMethodReflections() as $name => $reflection) { - $res->push(new RESTDocGenerator_ActionInspector($this->nounInspector, $this, $name, $reflection)); - } - return $res; - } - - +class RESTDocGenerator_HandlerInspector extends ViewableData +{ + + public function __construct($nounInspector, $handler) + { + $this->nounInspector = $nounInspector; + + $this->handler = $handler; + $this->reflection = new ReflectionClass($handler); + + $this->actionBlocks = array(); + foreach ($this->getActionMethodReflections() as $name => $reflection) { + $this->actionBlocks[$name] = RESTDocblockParser::parse($reflection); + } + + parent::__construct(); + } + + protected function getActionMethodReflections() + { + $allow = Object::get_static($this->handler, 'allowed_actions'); + $res = new RESTDocGenerator_MethodFilter($this->reflection); + + return $res->isSubclassOf('RESTNoun_Handler')->isCallablePublicMethod()->nameInArray($allow)->asArray(); + } + + public function getActions() + { + $res = new ArrayList(); + foreach ($this->getActionMethodReflections() as $name => $reflection) { + $res->push(new RESTDocGenerator_ActionInspector($this->nounInspector, $this, $name, $reflection)); + } + return $res; + } } /** * @package restassured */ -class RESTDocGenerator_NestingInspector extends ViewableData { - - protected $parent; - protected $link; - - function __construct($noun, $parent = null, $link = RESTASSURED_ROOT) { - // Store the class, and a reflector since we'll use those a lot - $this->noun = $noun; - $this->reflection = new ReflectionClass($noun); - - // Store the parent & link from parent to here, used to get URL & ID) - $this->parent = $parent; - $this->link = $link; - - // Parse the main class block - $this->classBlock = RESTDocblockParser::parse($this->reflection); - - // Parse the method blocks - $this->methodBlocks = array(); - foreach ($this->getPropertyMethodReflections() as $name => $methodReflection) { - $this->methodBlocks[$name] = RESTDocblockParser::parse($methodReflection); - } - - parent::__construct(); - } - - protected function getPropertyMethodReflections() { - $res = new RESTDocGenerator_MethodFilter($this->reflection); - return $res->isSubclassOf('RESTNoun')->isCallablePublicMethod()->startsWith('get')->hasNumberOfParameters(0)->asArray(); - } - - function getID() { - $id = Convert::raw2att($this->noun); - return ($this->parent) ? $this->parent->getID() . $id : $id; - } - - function getURL() { - return $this->parent ? Controller::join_links($this->parent->URL, $this->link) : $this->link; - } - - function getName() { - $stat = Object::get_static($this->noun, 'name'); - return $stat ? $stat : $this->noun; - } - - function getClass() { - return $this->noun; - } - - function getType() { - return is_subclass_of($this->noun, 'RESTItem') ? 'Item' : 'Collection'; - } - - function getDescription() { - return $this->classBlock['body']; - } - - function getNounChildrenStatic($stat) { - $res = array(); - - foreach (ClassInfo::ancestry($this->noun) as $class) { - if (!is_subclass_of($class, 'RESTNoun')) continue; - - $local = Object::uninherited_static($class, $stat); - - if ($local) { - if (is_array($res) && is_array($local)) $res = array_merge($res, $local); - else $res = $local; - } - } - - return $res; - } - - function getFields() { - $fields = array(); - - foreach($this->getNounChildrenStatic('casting') as $name => $type) { - $fields[$name] = new ArrayData(array('Name' => $name, 'Type' => $type)); - } - - foreach ($this->getPropertyMethodReflections() as $name => $reflection) { - $field = preg_replace('/^get/', '', $name); - $doc = $this->methodBlocks[$name]; - - // Decode the return type - $return = $doc['return']['details']; - $words = explode(' ', $return); - $type = count($words) ? $words[0] : ''; - - // If not in the fields yet (because it's not in $casting), add new type using return tag - if (!isset($fields[$field])) $fields[$field] = new ArrayData(array('Name' => $field, 'Type' => $type)); - - // Set the body - $fields[$field]->Description = $doc['body']; - } - - foreach ($fields as $name => $details) { - $type = preg_replace('/\[([^\]]+)\]/', '$1', $details->Type); - if (ClassInfo::is_subclass_of($type, 'RESTNoun')) { - $fields[$name]->Link = $type; - } - } - - return new ArrayList($fields); - } - - function getHandler() { - $ancestry = array_reverse(ClassInfo::ancestry($this->noun)); - foreach ($ancestry as $class) { - $class = $class . '_Handler'; - if (ClassInfo::exists($class)) return new RESTDocGenerator_HandlerInspector($this, $class); - } - } - - function getReturnedTypes() { - $classes = array(); - - if ($this->noun == 'RESTRoot') { - $classes = RESTRoot::get_registered(); - } - else { - foreach ($this->methodBlocks as $name => $block) { - $return = $block['return']['details']; $words = explode(' ', $return); $type = count($words) ? $words[0] : ''; - - $name = preg_replace('/^get([A-Z])/', '$1', $name); - if ($name == 'Items') $name = "{id}"; - $type = preg_replace('/^\[([^\]]+)\]$/', '$1', $type); - - - if ($type && ClassInfo::is_subclass_of($type, 'RESTNoun')) { - $classes[$name] = $type; - } - } - } - - // Build a new class inspector for each - $res = new ArrayList(); - foreach ($classes as $func => $class) { - $inspect = new RESTDocGenerator_NestingInspector($class, $this, $func); - $res->push($inspect); - } - - return $res; - } - - function getSubClasses($includeSelf = false) { - $res = new ArrayList(); - - foreach (ClassInfo::subclassesFor($this->noun) as $subClass) { - if ($subClass == $this->noun) { - if ($includeSelf) $res->push($this); - } - else { - $res->push(new RESTDocGenerator_NestingInspector($subClass, $this->parent, $this->link)); - } - } - - return $res; - } +class RESTDocGenerator_NestingInspector extends ViewableData +{ + + protected $parent; + protected $link; + + public function __construct($noun, $parent = null, $link = RESTASSURED_ROOT) + { + // Store the class, and a reflector since we'll use those a lot + $this->noun = $noun; + $this->reflection = new ReflectionClass($noun); + + // Store the parent & link from parent to here, used to get URL & ID) + $this->parent = $parent; + $this->link = $link; + + // Parse the main class block + $this->classBlock = RESTDocblockParser::parse($this->reflection); + + // Parse the method blocks + $this->methodBlocks = array(); + foreach ($this->getPropertyMethodReflections() as $name => $methodReflection) { + $this->methodBlocks[$name] = RESTDocblockParser::parse($methodReflection); + } + + parent::__construct(); + } + + protected function getPropertyMethodReflections() + { + $res = new RESTDocGenerator_MethodFilter($this->reflection); + return $res->isSubclassOf('RESTNoun')->isCallablePublicMethod()->startsWith('get')->hasNumberOfParameters(0)->asArray(); + } + + public function getID() + { + $id = Convert::raw2att($this->noun); + return ($this->parent) ? $this->parent->getID() . $id : $id; + } + + public function getURL() + { + return $this->parent ? Controller::join_links($this->parent->URL, $this->link) : $this->link; + } + + public function getName() + { + $stat = Object::get_static($this->noun, 'name'); + return $stat ? $stat : $this->noun; + } + + public function getClass() + { + return $this->noun; + } + + public function getType() + { + return is_subclass_of($this->noun, 'RESTItem') ? 'Item' : 'Collection'; + } + + public function getDescription() + { + return $this->classBlock['body']; + } + + public function getNounChildrenStatic($stat) + { + $res = array(); + + foreach (ClassInfo::ancestry($this->noun) as $class) { + if (!is_subclass_of($class, 'RESTNoun')) { + continue; + } + + $local = Object::uninherited_static($class, $stat); + + if ($local) { + if (is_array($res) && is_array($local)) { + $res = array_merge($res, $local); + } else { + $res = $local; + } + } + } + + return $res; + } + + public function getFields() + { + $fields = array(); + + foreach ($this->getNounChildrenStatic('casting') as $name => $type) { + $fields[$name] = new ArrayData(array('Name' => $name, 'Type' => $type)); + } + + foreach ($this->getPropertyMethodReflections() as $name => $reflection) { + $field = preg_replace('/^get/', '', $name); + $doc = $this->methodBlocks[$name]; + + // Decode the return type + $return = $doc['return']['details']; + $words = explode(' ', $return); + $type = count($words) ? $words[0] : ''; + + // If not in the fields yet (because it's not in $casting), add new type using return tag + if (!isset($fields[$field])) { + $fields[$field] = new ArrayData(array('Name' => $field, 'Type' => $type)); + } + + // Set the body + $fields[$field]->Description = $doc['body']; + } + + foreach ($fields as $name => $details) { + $type = preg_replace('/\[([^\]]+)\]/', '$1', $details->Type); + if (ClassInfo::is_subclass_of($type, 'RESTNoun')) { + $fields[$name]->Link = $type; + } + } + + return new ArrayList($fields); + } + + public function getHandler() + { + $ancestry = array_reverse(ClassInfo::ancestry($this->noun)); + foreach ($ancestry as $class) { + $class = $class . '_Handler'; + if (ClassInfo::exists($class)) { + return new RESTDocGenerator_HandlerInspector($this, $class); + } + } + } + + public function getReturnedTypes() + { + $classes = array(); + + if ($this->noun == 'RESTRoot') { + $classes = RESTRoot::get_registered(); + } else { + foreach ($this->methodBlocks as $name => $block) { + $return = $block['return']['details']; + $words = explode(' ', $return); + $type = count($words) ? $words[0] : ''; + + $name = preg_replace('/^get([A-Z])/', '$1', $name); + if ($name == 'Items') { + $name = "{id}"; + } + $type = preg_replace('/^\[([^\]]+)\]$/', '$1', $type); + + + if ($type && ClassInfo::is_subclass_of($type, 'RESTNoun')) { + $classes[$name] = $type; + } + } + } + + // Build a new class inspector for each + $res = new ArrayList(); + foreach ($classes as $func => $class) { + $inspect = new RESTDocGenerator_NestingInspector($class, $this, $func); + $res->push($inspect); + } + + return $res; + } + + public function getSubClasses($includeSelf = false) + { + $res = new ArrayList(); + + foreach (ClassInfo::subclassesFor($this->noun) as $subClass) { + if ($subClass == $this->noun) { + if ($includeSelf) { + $res->push($this); + } + } else { + $res->push(new RESTDocGenerator_NestingInspector($subClass, $this->parent, $this->link)); + } + } + + return $res; + } } -class RESTDocGenerator_TypeInspector extends ViewableData { - - protected $typeList = array(); - - function getTypes() { - $this->recursivelyCollectTypes(new RESTDocGenerator_NestingInspector('RESTRoot')); - return new ArrayList($this->typeList); - } - - function recursivelyCollectTypes($base) { - foreach ($base->getSubClasses(true) as $inspector) { - foreach ($inspector->getReturnedTypes() as $returned) { - $noun = $returned->noun; - - if (!isset($this->typeList[$noun])) { - $this->typeList[$noun] = $returned; - $this->recursivelyCollectTypes($returned); - } - } - - foreach ($inspector->getHandler()->getActions() as $action) { - foreach ($action->getReturnedTypes() as $returned) { - $noun = $returned->noun; - - if (!isset($this->typeList[$noun])) { - $this->typeList[$noun] = $returned; - $this->recursivelyCollectTypes($returned); - } - } - } - } - } - +class RESTDocGenerator_TypeInspector extends ViewableData +{ + + protected $typeList = array(); + + public function getTypes() + { + $this->recursivelyCollectTypes(new RESTDocGenerator_NestingInspector('RESTRoot')); + return new ArrayList($this->typeList); + } + + public function recursivelyCollectTypes($base) + { + foreach ($base->getSubClasses(true) as $inspector) { + foreach ($inspector->getReturnedTypes() as $returned) { + $noun = $returned->noun; + + if (!isset($this->typeList[$noun])) { + $this->typeList[$noun] = $returned; + $this->recursivelyCollectTypes($returned); + } + } + + foreach ($inspector->getHandler()->getActions() as $action) { + foreach ($action->getReturnedTypes() as $returned) { + $noun = $returned->noun; + + if (!isset($this->typeList[$noun])) { + $this->typeList[$noun] = $returned; + $this->recursivelyCollectTypes($returned); + } + } + } + } + } } /** * @package restassured */ -class RESTDocGenerator extends BuildTask { - static function render() { - $data = new ArrayData(array( - 'Types' => new RESTDocGenerator_TypeInspector(), - 'Actions' => new RESTDocGenerator_NestingInspector('RESTRoot') - )); - - return $data->renderWith('RESTDocGenerator'); - } - - function run($request) { - file_put_contents(ASSETS_PATH . '/apidocs.html', self::render()); - } +class RESTDocGenerator extends BuildTask +{ + public static function render() + { + $data = new ArrayData(array( + 'Types' => new RESTDocGenerator_TypeInspector(), + 'Actions' => new RESTDocGenerator_NestingInspector('RESTRoot') + )); + + return $data->renderWith('RESTDocGenerator'); + } + + public function run($request) + { + file_put_contents(ASSETS_PATH . '/apidocs.html', self::render()); + } } /** * @package restassured */ -class RESTDocGenerator_Controller extends Controller { - function index() { - return RESTDocGenerator::render(); - } -} \ No newline at end of file +class RESTDocGenerator_Controller extends Controller +{ + public function index() + { + return RESTDocGenerator::render(); + } +} diff --git a/code/RESTDocblockParser.php b/code/RESTDocblockParser.php index 34be35c..0586b91 100644 --- a/code/RESTDocblockParser.php +++ b/code/RESTDocblockParser.php @@ -23,74 +23,87 @@ * * @package restassured */ -class RESTDocblockParser implements arrayaccess { - static function parse($source) { - $string = $source; - - if ($source instanceof Reflector) $string = preg_replace('{^[ \t]*(/\*+|\*+/?)}m', '', $source->getDocComment()); - - $lines = preg_split('/^/m', $string); - return new RESTDocblockParser($lines, -1); - } - - function __construct(&$lines, $level) { - $res = array(); - $body = array(); - - while (count($lines)) { - $line = array_shift($lines); - - // Blank lines just get a carriage return added (for markdown) and otherwise ignored - if (!trim($line)) { - $body[] = "\n"; - continue; - } - - // Get the indent - preg_match('/^(\s*)/', $line, $match); - $indent = $match[1]; - - // Check to make sure we're still indented - if (strlen($indent) <= $level) { - array_unshift($lines, $line); - break; - } - - // Check for tag - if (preg_match('/^@([^\s]+)(.*)$/', trim($line), $match)) { - $tag = $match[1]; - - $sub = new RESTDocblockParser($lines, strlen($indent)); - $sub['details'] = trim($match[2]); - - if (!isset($res[$tag])) $res[$tag] = new RESTDocblockParser_Sequence(); - $res[$tag][] = $sub; - } - else { - $body[] = substr($line, $level > 0 ? $level : 0); - } - } - - $res['body'] = Markdown(implode("", $body)); - - $this->res = $res; - } - - public function offsetExists ($offset) { - return isset($this->res[$offset]); - } - - public function offsetGet ($offset) { - if (isset($this->res[$offset])) return $this->res[$offset]; - // No match, return the null - return singleton('RESTDocblockParser_Null'); - } - - public function offsetSet ($offset, $value) { - $this->res[$offset] = $value; - } - - public function offsetUnset ($offset) { /* NOP */ } +class RESTDocblockParser implements arrayaccess +{ + public static function parse($source) + { + $string = $source; + + if ($source instanceof Reflector) { + $string = preg_replace('{^[ \t]*(/\*+|\*+/?)}m', '', $source->getDocComment()); + } + + $lines = preg_split('/^/m', $string); + return new RESTDocblockParser($lines, -1); + } + + public function __construct(&$lines, $level) + { + $res = array(); + $body = array(); + + while (count($lines)) { + $line = array_shift($lines); + + // Blank lines just get a carriage return added (for markdown) and otherwise ignored + if (!trim($line)) { + $body[] = "\n"; + continue; + } + + // Get the indent + preg_match('/^(\s*)/', $line, $match); + $indent = $match[1]; + + // Check to make sure we're still indented + if (strlen($indent) <= $level) { + array_unshift($lines, $line); + break; + } + + // Check for tag + if (preg_match('/^@([^\s]+)(.*)$/', trim($line), $match)) { + $tag = $match[1]; + + $sub = new RESTDocblockParser($lines, strlen($indent)); + $sub['details'] = trim($match[2]); + + if (!isset($res[$tag])) { + $res[$tag] = new RESTDocblockParser_Sequence(); + } + $res[$tag][] = $sub; + } else { + $body[] = substr($line, $level > 0 ? $level : 0); + } + } + + $res['body'] = Markdown(implode("", $body)); + + $this->res = $res; + } + + public function offsetExists($offset) + { + return isset($this->res[$offset]); + } + + public function offsetGet($offset) + { + if (isset($this->res[$offset])) { + return $this->res[$offset]; + } + // No match, return the null + return singleton('RESTDocblockParser_Null'); + } + + public function offsetSet($offset, $value) + { + $this->res[$offset] = $value; + } + + public function offsetUnset($offset) + { /* NOP */ + } } /** @@ -109,37 +122,49 @@ public function offsetUnset ($offset) { /* NOP */ } * * @package restassured */ -class RESTDocblockParser_Sequence implements arrayaccess, IteratorAggregate { - - protected $seq = array(); - - public function offsetExists ($offset) { - return isset($this->seq[$offset]); - } - - public function offsetGet ($offset) { - if (is_numeric($offset)) { - if (isset($this->seq[$offset])) return $this->seq[$offset]; - } - else { - if (isset($this->seq[0])) return $this->seq[0][$offset]; - } - // No match, return the null - return singleton('RESTDocblockParser_Null'); - } - - public function offsetSet ($offset, $value) { - if ($offset === null) $this->seq[] = $value; - else $this->seq[$offset] = $value; - } - - public function offsetUnset ($offset) { - unset($this->seq[$offset]); - } - - public function getIterator() { +class RESTDocblockParser_Sequence implements arrayaccess, IteratorAggregate +{ + + protected $seq = array(); + + public function offsetExists($offset) + { + return isset($this->seq[$offset]); + } + + public function offsetGet($offset) + { + if (is_numeric($offset)) { + if (isset($this->seq[$offset])) { + return $this->seq[$offset]; + } + } else { + if (isset($this->seq[0])) { + return $this->seq[0][$offset]; + } + } + // No match, return the null + return singleton('RESTDocblockParser_Null'); + } + + public function offsetSet($offset, $value) + { + if ($offset === null) { + $this->seq[] = $value; + } else { + $this->seq[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->seq[$offset]); + } + + public function getIterator() + { return new ArrayIterator($this->seq); - } + } } /** @@ -148,16 +173,37 @@ public function getIterator() { * * @package restassured */ -class RESTDocblockParser_Null implements arrayaccess, IteratorAggregate { - function __construct() {} - - public function offsetExists ($offset) { return false; } - public function offsetGet ($offset) { return $this; } - public function offsetSet ($offset, $value) { /* NOP */ } - public function offsetUnset ($offset) { /* NOP */ } - public function getIterator() { return new EmptyIterator(); } - - function __toString() { return ''; } - function __toBool() { return false; } +class RESTDocblockParser_Null implements arrayaccess, IteratorAggregate +{ + public function __construct() + { + } + + public function offsetExists($offset) + { + return false; + } + public function offsetGet($offset) + { + return $this; + } + public function offsetSet($offset, $value) + { /* NOP */ + } + public function offsetUnset($offset) + { /* NOP */ + } + public function getIterator() + { + return new EmptyIterator(); + } + + public function __toString() + { + return ''; + } + public function __toBool() + { + return false; + } } - diff --git a/code/RESTException.php b/code/RESTException.php index 0ef8c5d..a04ebb4 100644 --- a/code/RESTException.php +++ b/code/RESTException.php @@ -1,15 +1,16 @@ getAcceptMimetypes(); - // Alternatively the type might be specified by the client as an extension on the URL - $extension = $request->getExtension(); - } - // Request can alternatively be a string which might be a mimetype or an extension - else { - $mimetypes = array($request); $extension = $request; - } - - // Filter out empty items and */* - $mimetypes = array_filter($mimetypes, array(__CLASS__, 'useful_mimetype')); - - // If we didn't get a mimetype _or_ an extension, use the default - if (!$mimetypes && !$extension) $mimetypes = array(self::$default); - - // Now step through looking for matches on any specified mimetype or exception - $byMimeType = null; $byExtension = null; - - foreach (ClassInfo::subclassesFor(__CLASS__) as $class) { - if ($class == __CLASS__) continue; - - if($mimetypes && count(array_intersect($mimetypes, Object::get_static($class, 'mimetypes')))) { - $byMimeType = $class; - break; // Mimetypes take priority over extensions. If we get a match we're done. - } - - if($extension && in_array($extension, Object::get_static($class, 'url_extensions'))) { - $byExtension = $class; - if (!$mimetypes) break; // We're only done on a match if we don't have a mimetype to look for. - } - } - - // Mime type match gets priority over extension - if ($byMimeType) return new $byMimeType(); - if ($byExtension) return new $byExtension(); - } - - /** - * Takes a possibly nested set of stdClass objects and turns it into a nested associative array - * @static - * @param $d stdClass - The object to convert - * @return array - An array with every property of the passed object converted to a key => value pair in the array recursively - */ - private static function object_to_array($d) { - if (is_object($d)) { - $d = get_object_vars($d); - } - - if (is_array($d)) { - return array_map(array(__CLASS__, __FUNCTION__), $d); - } - else { - return $d; - } - } - - /** - * Takes a list of fields - a list of "." seperated strings - and turns it into a fieldspec (a nested associative array), - * where the key is the field, and the value is an array of nested fields or false if no nesting, i.e. - * - * array('Foo', 'Bar.A', 'Bar.B') - * - * becomes - * - * array( - * 'Foo' => false, - * 'Bar' => array( - * 'A' => false, - * 'B' => false - * ) - * ); - * - * @param $fields [string] - the list of fields - * @return array - th - */ - function decodeFields($fields) { - /* Turn fields (a list of "." separated strings) into a field spec (a nested array) */ - - $fieldspec = new stdClass(); - - foreach ($fields as $field) { - $parts = explode('.', $field); - $dest = $fieldspec; - - while(count($parts) > 1) { - $part = array_shift($parts); - - if (!isset($dest->$part)) $dest->$part = new stdClass(); - $dest = $dest->$part; - } - - $part = $parts[0]; - $dest->$part = false; - } - - return self::object_to_array($fieldspec); - } - - /** - * format is the main public entry point for formatting RESTNouns as output - * @param $noun - * @param $fields - * @return void - */ - function format($noun, $fields) { - if (!is_array($fields)) $fields = preg_split('/[,\s]+/', $fields); - - $data = $this->collectFields($noun, $this->decodeFields($fields)); - return $this->buildResponse($noun, $data); - } - - - - /** - * Given a fieldspec and a noun, recursively collect the specified fields into a "data" element - a subclass specific - * object that can then be trivially converted into the expected response. - * @param $noun RESTNoun - The noun we're currently collecting fields from - * @param $fieldspec array - The nested fields specification that is the result of #format's conversion from a set of "." separated strings - * @return any - An opaque object that is understood by the particular buildResponse method of the subclass - */ - protected function collectFields($noun, $fieldspec) { - $res = $this->dataItem($noun->class); - - if (array_key_exists('*', $fieldspec)) { - $fieldspec = array_merge($fieldspec, $this->decodeFields(Config::inst()->get($noun->class, 'default_fields'))); - unset($fieldspec['*']); - } - - foreach ($fieldspec as $field => $nesting) { - if ($nesting) { - $sub = $noun->$field; - if (is_array($sub)) { - $col = $this->addCollectionToItem($res, $field); - foreach ($noun->$field as $item) $this->appendToCollection($res, $col, $field, $this->collectFields($item, $nesting)); - } - elseif($sub) { - $this->addToItem($res, $field, $this->collectFields($noun->$field, $nesting)); - } - } - else { - $this->addToItem($res, $field, $noun->$field); - } - } - - return $res; - } - - /** - * These methods are overridden by the specific formatter subclasses - */ - - /** Given a class as a string, return a "data" item - some object that can hold data during collection */ - abstract function dataItem($class); - /** Add a key / value pair to an object as returned by dataItem */ - abstract function addToItem($dataItem, $field, $value); - - /** - * Works in concert with #appendToCollection to handle sequences. - * This is called once to allow the construction of any sequence object, then #appendToCollection is called repeatedly with - * the same arguments, plus any handle this function returns and the values to add - * @abstract - * @param $dataItem any - The object as returned by #dataItem to add a sequence to - * @param $field string - The name of the sequence in the object - * @return any - An optional handle. Will be passed to #appendToCollection without change - */ - abstract function addCollectionToItem($dataItem, $field); - - /** - * Add a value to a sequence - * @abstract - * @param $dataItem any - same as passed to #addCollectionToItem - * @param $dataCollection any - handle returned from #addCollectionToItem - * @param $field string - same as passed to #addCollectionToItem - * @param $value any - value to add to sequence - * @return void - */ - abstract function appendToCollection($dataItem, $dataCollection, $field, $value); - - /** - * Takes the noun we build the data from and the data as built by collectFields, and returns an HTTPResponse object - * that contains the finally formatted data - * @abstract - * @param $noun RESTNoun - the noun as passed to #format - * @param $data any - the data element as generated by #dataItem inside #collectFields - * @return SS_HTTPResponse - the response - */ - abstract protected function buildResponse($noun, $data); +abstract class RESTFormatter +{ + + public static $default = 'application/json'; + + private static function useful_mimetype($type) + { + return $type && $type != '*/*'; + } + + /** + * Factory that returns a RESTFormatter subclass that formats in a particular format based on mimetype or extension + * + * @static + * @param SS_HTTPRequest | string - $request + * @return RESTFormatter - an instance of a RESTFormatter that can do the job, or null if none can. + */ + public static function get_formatter($request = null) + { + // Request can be an SS_HTTPRequest object + if ($request instanceof SS_HTTPRequest) { + // Try and get the mimetype from the request's Accept header + $mimetypes = $request->getAcceptMimetypes(); + // Alternatively the type might be specified by the client as an extension on the URL + $extension = $request->getExtension(); + } + // Request can alternatively be a string which might be a mimetype or an extension + else { + $mimetypes = array($request); + $extension = $request; + } + + // Filter out empty items and */* + $mimetypes = array_filter($mimetypes, array(__CLASS__, 'useful_mimetype')); + + // If we didn't get a mimetype _or_ an extension, use the default + if (!$mimetypes && !$extension) { + $mimetypes = array(self::$default); + } + + // Now step through looking for matches on any specified mimetype or exception + $byMimeType = null; + $byExtension = null; + + foreach (ClassInfo::subclassesFor(__CLASS__) as $class) { + if ($class == __CLASS__) { + continue; + } + + if ($mimetypes && count(array_intersect($mimetypes, Object::get_static($class, 'mimetypes')))) { + $byMimeType = $class; + break; // Mimetypes take priority over extensions. If we get a match we're done. + } + + if ($extension && in_array($extension, Object::get_static($class, 'url_extensions'))) { + $byExtension = $class; + if (!$mimetypes) { + break; + } // We're only done on a match if we don't have a mimetype to look for. + } + } + + // Mime type match gets priority over extension + if ($byMimeType) { + return new $byMimeType(); + } + if ($byExtension) { + return new $byExtension(); + } + } + + /** + * Takes a possibly nested set of stdClass objects and turns it into a nested associative array + * @static + * @param $d stdClass - The object to convert + * @return array - An array with every property of the passed object converted to a key => value pair in the array recursively + */ + private static function object_to_array($d) + { + if (is_object($d)) { + $d = get_object_vars($d); + } + + if (is_array($d)) { + return array_map(array(__CLASS__, __FUNCTION__), $d); + } else { + return $d; + } + } + + /** + * Takes a list of fields - a list of "." seperated strings - and turns it into a fieldspec (a nested associative array), + * where the key is the field, and the value is an array of nested fields or false if no nesting, i.e. + * + * array('Foo', 'Bar.A', 'Bar.B') + * + * becomes + * + * array( + * 'Foo' => false, + * 'Bar' => array( + * 'A' => false, + * 'B' => false + * ) + * ); + * + * @param $fields [string] - the list of fields + * @return array - th + */ + public function decodeFields($fields) + { + /* Turn fields (a list of "." separated strings) into a field spec (a nested array) */ + + $fieldspec = new stdClass(); + + foreach ($fields as $field) { + $parts = explode('.', $field); + $dest = $fieldspec; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (!isset($dest->$part)) { + $dest->$part = new stdClass(); + } + $dest = $dest->$part; + } + + $part = $parts[0]; + $dest->$part = false; + } + + return self::object_to_array($fieldspec); + } + + /** + * format is the main public entry point for formatting RESTNouns as output + * @param $noun + * @param $fields + * @return void + */ + public function format($noun, $fields) + { + if (!is_array($fields)) { + $fields = preg_split('/[,\s]+/', $fields); + } + + $data = $this->collectFields($noun, $this->decodeFields($fields)); + return $this->buildResponse($noun, $data); + } + + + + /** + * Given a fieldspec and a noun, recursively collect the specified fields into a "data" element - a subclass specific + * object that can then be trivially converted into the expected response. + * @param $noun RESTNoun - The noun we're currently collecting fields from + * @param $fieldspec array - The nested fields specification that is the result of #format's conversion from a set of "." separated strings + * @return any - An opaque object that is understood by the particular buildResponse method of the subclass + */ + protected function collectFields($noun, $fieldspec) + { + $res = $this->dataItem($noun->class); + + if (array_key_exists('*', $fieldspec)) { + $fieldspec = array_merge($fieldspec, $this->decodeFields(Config::inst()->get($noun->class, 'default_fields'))); + unset($fieldspec['*']); + } + + foreach ($fieldspec as $field => $nesting) { + if ($nesting) { + $sub = $noun->$field; + if (is_array($sub)) { + $col = $this->addCollectionToItem($res, $field); + foreach ($noun->$field as $item) { + $this->appendToCollection($res, $col, $field, $this->collectFields($item, $nesting)); + } + } elseif ($sub) { + $this->addToItem($res, $field, $this->collectFields($noun->$field, $nesting)); + } + } else { + $this->addToItem($res, $field, $noun->$field); + } + } + + return $res; + } + + /** + * These methods are overridden by the specific formatter subclasses + */ + + /** Given a class as a string, return a "data" item - some object that can hold data during collection */ + abstract public function dataItem($class); + /** Add a key / value pair to an object as returned by dataItem */ + abstract public function addToItem($dataItem, $field, $value); + + /** + * Works in concert with #appendToCollection to handle sequences. + * This is called once to allow the construction of any sequence object, then #appendToCollection is called repeatedly with + * the same arguments, plus any handle this function returns and the values to add + * @abstract + * @param $dataItem any - The object as returned by #dataItem to add a sequence to + * @param $field string - The name of the sequence in the object + * @return any - An optional handle. Will be passed to #appendToCollection without change + */ + abstract public function addCollectionToItem($dataItem, $field); + + /** + * Add a value to a sequence + * @abstract + * @param $dataItem any - same as passed to #addCollectionToItem + * @param $dataCollection any - handle returned from #addCollectionToItem + * @param $field string - same as passed to #addCollectionToItem + * @param $value any - value to add to sequence + * @return void + */ + abstract public function appendToCollection($dataItem, $dataCollection, $field, $value); + + /** + * Takes the noun we build the data from and the data as built by collectFields, and returns an HTTPResponse object + * that contains the finally formatted data + * @abstract + * @param $noun RESTNoun - the noun as passed to #format + * @param $data any - the data element as generated by #dataItem inside #collectFields + * @return SS_HTTPResponse - the response + */ + abstract protected function buildResponse($noun, $data); } -class RESTFormatter_JSON extends RESTFormatter { - static $mimetypes = array('application/json'); - static $url_extensions = array('js', 'json'); - - static $type_attribute = '$type'; - - function dataItem($class) { - $res = new stdClass(); - - $field = self::$type_attribute; - if ($field) $res->$field = $class; - - return $res; - } - - function addToItem($dataItem, $field, $value) { - $dataItem->$field = $value; - } - - function addCollectionToItem($dataItem, $field) { - $dataItem->$field = array(); - } - - function appendToCollection($dataItem, $dataCollection, $field, $value) { - $array =& $dataItem->$field; - $array[] = $value; - } - - function buildResponse($noun, $data) { - $response = new SS_HTTPResponse(json_encode($data)); - $response->addHeader('Content-Type', 'application/json'); - - return $response; - } +class RESTFormatter_JSON extends RESTFormatter +{ + public static $mimetypes = array('application/json'); + public static $url_extensions = array('js', 'json'); + + public static $type_attribute = '$type'; + + public function dataItem($class) + { + $res = new stdClass(); + + $field = self::$type_attribute; + if ($field) { + $res->$field = $class; + } + + return $res; + } + + public function addToItem($dataItem, $field, $value) + { + $dataItem->$field = $value; + } + + public function addCollectionToItem($dataItem, $field) + { + $dataItem->$field = array(); + } + + public function appendToCollection($dataItem, $dataCollection, $field, $value) + { + $array =& $dataItem->$field; + $array[] = $value; + } + + public function buildResponse($noun, $data) + { + $response = new SS_HTTPResponse(json_encode($data)); + $response->addHeader('Content-Type', 'application/json'); + + return $response; + } } -class RESTFormatter_XML extends RESTFormatter { - static $mimetypes = array('text/xml'); - static $url_extensions = array('xml'); - - static $scalar_style = 'elem'; // 'elem' or 'attr' - - function format($noun, $fields) { - $this->document = new DOMDocument(); - return parent::format($noun, $fields); - } - - function dataItem($class) { - return $this->document->createElement($class); - } - - function addToItem($dataItem, $field, $value) { - if ($value instanceof DOMNode) { - $dataItem->appendChild($value); - } - elseif(self::$scalar_style == 'elem') { - $sub = $this->document->createElement($field, $value); - $dataItem->appendChild($sub); - } - else { - $dataItem->setAttribute($field, $value); - } - } - - function addCollectionToItem($dataItem, $field) { - $node = $this->document->createElement($field); - $dataItem->appendChild($node); - return $node; - } - - function appendToCollection($dataItem, $dataCollection, $field, $value) { - $dataCollection->appendChild($value); - } - - function buildResponse($noun, $data) { - $this->document->appendChild($data); - $response = new SS_HTTPResponse($this->document->saveXML()); - $response->addHeader('Content-Type', 'text/xml'); - - return $response; - } +class RESTFormatter_XML extends RESTFormatter +{ + public static $mimetypes = array('text/xml'); + public static $url_extensions = array('xml'); + + public static $scalar_style = 'elem'; // 'elem' or 'attr' + + public function format($noun, $fields) + { + $this->document = new DOMDocument(); + return parent::format($noun, $fields); + } + + public function dataItem($class) + { + return $this->document->createElement($class); + } + + public function addToItem($dataItem, $field, $value) + { + if ($value instanceof DOMNode) { + $dataItem->appendChild($value); + } elseif (self::$scalar_style == 'elem') { + $sub = $this->document->createElement($field, $value); + $dataItem->appendChild($sub); + } else { + $dataItem->setAttribute($field, $value); + } + } + + public function addCollectionToItem($dataItem, $field) + { + $node = $this->document->createElement($field); + $dataItem->appendChild($node); + return $node; + } + + public function appendToCollection($dataItem, $dataCollection, $field, $value) + { + $dataCollection->appendChild($value); + } + + public function buildResponse($noun, $data) + { + $this->document->appendChild($data); + $response = new SS_HTTPResponse($this->document->saveXML()); + $response->addHeader('Content-Type', 'text/xml'); + + return $response; + } } - - diff --git a/code/RESTItem.php b/code/RESTItem.php index 4abde89..45cd49d 100644 --- a/code/RESTItem.php +++ b/code/RESTItem.php @@ -8,22 +8,28 @@ * - Collection only has one type of Item nested under it, but it can have multiple of that type of Item * - Item has many types of Nouns (Collections & Items) nested under it, but it can only have one of each type of Noun */ -trait RESTItem { - use RESTNoun; +trait RESTItem +{ + use RESTNoun; - abstract function getID(); + abstract public function getID(); - function LinkFor($item) { - if ($item->parent !== $this) user_error('Tried to get link for noun that is not nested with this item', E_USER_ERROR); - return Controller::join_links($this->Link(), $item->linkFragment); - } + public function LinkFor($item) + { + if ($item->parent !== $this) { + user_error('Tried to get link for noun that is not nested with this item', E_USER_ERROR); + } + return Controller::join_links($this->Link(), $item->linkFragment); + } - protected function markAsNested($obj, $call) { - $obj->parent = $this; - $obj->linkFragment = $call; - return $obj; - } + protected function markAsNested($obj, $call) + { + $obj->parent = $this; + $obj->linkFragment = $call; + return $obj; + } } -class RESTItem_Handler extends RESTNoun_Handler { -} \ No newline at end of file +class RESTItem_Handler extends RESTNoun_Handler +{ +} diff --git a/code/RESTNoun.php b/code/RESTNoun.php index 2b16407..9367e0e 100644 --- a/code/RESTNoun.php +++ b/code/RESTNoun.php @@ -1,200 +1,250 @@ parent->LinkFor($this); - } + public function Link() + { + return $this->parent->LinkFor($this); + } - abstract function LinkFor($item); + abstract public function LinkFor($item); } -class RESTNoun_Handler extends RequestHandler { - function __construct($noun) { - $this->failover = $noun; - parent::__construct(); - } - - protected $request; - - function handleRequest(SS_HTTPRequest $request, DataModel $model) { - $this->request = $request; - - $method = $request->httpMethod(); - - if($this->checkAccessAction($method)) { - try { - $request = $this->$method($request); - // TODO: Abstract this out to API module, as it's application specific - Session::save(); - return $request; - } catch (Exception $e) { - if ($e instanceof SS_HTTPResponse_Exception) { - throw $e; - } elseif ($e instanceof RESTException) { - $this->respondWithError(array('code' => $e->getCode(), 'exception' => $e)); - } else { - $this->respondWithError(array('code' => 500, 'exception' => $e)); - } - } - } - $this->respondWithError(array('code' => 403, 'exception' => new Exception('Method not allowed'))); - } - - protected function parseRequest($args) { - // Make args' keys lowercase - $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); - - // Deal with request specially, since we need it to determine the default noun in the next step - $request = isset($args['request']) ? $args['request'] : $this->request; - - // Add defaults - $args = array_merge(array( - // Where to decode into. On PATCH, defaults to existing object. Otherwise defaults to empty - 'noun' => ($request->httpMethod() == 'PATCH' ? $this->failover : null), - // Default type to create - 'defaulttype' => null, - // Fields specification - 'fields' => array('*') - ), $args); - - $parser = RESTParser::get_parser($request); - if (!$parser) $this->respondWithError(array('code' => 415, 'exception' => new Exception('Couldnt find parser for body'))); - - $noun = $parser->parseInto($request, $args['fields'], $args['noun'], $args['defaulttype']); - return $noun; - } - - protected $headers = array(); - - protected function addResponseHeader($header, $value) { - $this->headers[] = array($header, $value); - } - - protected function resetResponseHeaders() { - $this->headers = array(); - } - - protected function respondAs($args) { - // Make args' keys lowercase - $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); - - // Add defaults - $args = array_merge(array( - // HTTP response code & description. - 'code' => 200, - 'codeDescription' => null, - // Specify a location to set as a Location: header. Useful if you make code a 3xx and don't want to call addHeader - 'location' => null, - // Response body. Overrides the calculation from noun - 'body' => null, - // Noun to respond with - 'noun' => $this->failover, - 'fields' => '*' - ), $args); - - $response = new SS_HTTPResponse(); - - // If a noun was provided, do the conversion - if ($args['noun']) { - // First. find a formatter. Give a 406 (not acceptable) if we can't find one - $formatter = RESTFormatter::get_formatter($this->request); - if (!$formatter) $this->respondWithError(array('code' => 406, 'exception' => new Exception('Couldnt find formatter for response'))); - - // Split the fields if it's a string - $fields = $args['fields']; - if (!is_array($fields)) $fields = preg_split('/[,\s]+/', $fields); - - // Format the response, and throw a 500 is we didn't get anything - $response = $formatter->format($args['noun'], $fields); - if (!$response) $this->respondWithError(array('code' => 500, 'exception' => new Exception('Response formatter returned NULL'))); - } - - // If a specific body was provided, use that - if ($args['body']) $response->setBody($args['body']); - - // Use the code specified - if ($args['code']) $response->setStatusCode($args['code'], $args['codeDescription']); - - // Add any headers - foreach ($this->headers as $header) $response->addHeader($header[0], $header[1]); - - // And then the location header - if ($args['location']) $response->addHeader('Location', Director::absoluteURL($args['location'], true)); - - // Clean up any pre-set headers - foreach (headers_list() as $header) { - $parts = explode(':', $header); - $name = trim($parts[0]); - - if (function_exists('header_remove')) header_remove($name); - else header($name.':'); - } - - return $response; - } - - protected function respondWith() { - return $this->respondAs(array('fields' => func_get_args())); - } - - public static $exception_noun; - - public function respondWithError($args) { - // Make args' keys lowercase - $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); - - // See if an exception is passed (no default) - $exception = null; - if (isset($args['exception'])) $exception = $args['exception']; - - // Get the default exception noun if set - $exceptionNoun = self::$exception_noun; - - // Add defaults - $args = array_merge(array( - // HTTP response code & description. - 'code' => 500, - 'description' => null, - // Response body. Overrides the calculation from noun - 'body' => null, - // Noun to respond with - 'noun' => ($exception && $exceptionNoun) ? new $exceptionNoun($exception) : null - ), $args); - - // Default "response" which we build into. Not returned, because the exception has it's own response - $response = new SS_HTTPResponse(); - - // If there's a formatter - if ($args['noun'] && ($formatter = RESTFormatter::get_formatter($this->request))) { - // Format the response. Revert back to default response if we got nothing. - $response = $formatter->format($args['noun'], array('*')); - if (!$response) $response = new SS_HTTPResponse(); - } - else if ($exception) { - $exception = $args['exception']; - $response->setBody($exception->getMessage()."\n"); - $response->addHeader('Content-Type', 'text/plain'); - } - - // If a specific body was provided, use that - if ($args['body']) $response->setBody($args['body']); - - // Build an exception with those details - $e = new SS_HTTPResponse_Exception($response->getBody(), $args['code'], $args['description']); - $exceptionResponse = $e->getResponse(); - - // Add user specified headers - foreach ($response->getHeaders() as $k => $v) $exceptionResponse->addHeader($k, $v); - - throw $e; - } - - public function GET($request) { $this->httpError(403); } - public function POST($request) { $this->httpError(403); } - public function PUT($request) { $this->httpError(403); } - public function DELETE($request) { $this->httpError(403); } +class RESTNoun_Handler extends RequestHandler +{ + public function __construct($noun) + { + $this->failover = $noun; + parent::__construct(); + } + + protected $request; + + public function handleRequest(SS_HTTPRequest $request, DataModel $model) + { + $this->request = $request; + + $method = $request->httpMethod(); + + if ($this->checkAccessAction($method)) { + try { + $request = $this->$method($request); + // TODO: Abstract this out to API module, as it's application specific + Session::save(); + return $request; + } catch (Exception $e) { + if ($e instanceof SS_HTTPResponse_Exception) { + throw $e; + } elseif ($e instanceof RESTException) { + $this->respondWithError(array('code' => $e->getCode(), 'exception' => $e)); + } else { + $this->respondWithError(array('code' => 500, 'exception' => $e)); + } + } + } + $this->respondWithError(array('code' => 403, 'exception' => new Exception('Method not allowed'))); + } + + protected function parseRequest($args) + { + // Make args' keys lowercase + $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); + + // Deal with request specially, since we need it to determine the default noun in the next step + $request = isset($args['request']) ? $args['request'] : $this->request; + + // Add defaults + $args = array_merge(array( + // Where to decode into. On PATCH, defaults to existing object. Otherwise defaults to empty + 'noun' => ($request->httpMethod() == 'PATCH' ? $this->failover : null), + // Default type to create + 'defaulttype' => null, + // Fields specification + 'fields' => array('*') + ), $args); + + $parser = RESTParser::get_parser($request); + if (!$parser) { + $this->respondWithError(array('code' => 415, 'exception' => new Exception('Couldnt find parser for body'))); + } + + $noun = $parser->parseInto($request, $args['fields'], $args['noun'], $args['defaulttype']); + return $noun; + } + + protected $headers = array(); + + protected function addResponseHeader($header, $value) + { + $this->headers[] = array($header, $value); + } + + protected function resetResponseHeaders() + { + $this->headers = array(); + } + + protected function respondAs($args) + { + // Make args' keys lowercase + $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); + + // Add defaults + $args = array_merge(array( + // HTTP response code & description. + 'code' => 200, + 'codeDescription' => null, + // Specify a location to set as a Location: header. Useful if you make code a 3xx and don't want to call addHeader + 'location' => null, + // Response body. Overrides the calculation from noun + 'body' => null, + // Noun to respond with + 'noun' => $this->failover, + 'fields' => '*' + ), $args); + + $response = new SS_HTTPResponse(); + + // If a noun was provided, do the conversion + if ($args['noun']) { + // First. find a formatter. Give a 406 (not acceptable) if we can't find one + $formatter = RESTFormatter::get_formatter($this->request); + if (!$formatter) { + $this->respondWithError(array('code' => 406, 'exception' => new Exception('Couldnt find formatter for response'))); + } + + // Split the fields if it's a string + $fields = $args['fields']; + if (!is_array($fields)) { + $fields = preg_split('/[,\s]+/', $fields); + } + + // Format the response, and throw a 500 is we didn't get anything + $response = $formatter->format($args['noun'], $fields); + if (!$response) { + $this->respondWithError(array('code' => 500, 'exception' => new Exception('Response formatter returned NULL'))); + } + } + + // If a specific body was provided, use that + if ($args['body']) { + $response->setBody($args['body']); + } + + // Use the code specified + if ($args['code']) { + $response->setStatusCode($args['code'], $args['codeDescription']); + } + + // Add any headers + foreach ($this->headers as $header) { + $response->addHeader($header[0], $header[1]); + } + + // And then the location header + if ($args['location']) { + $response->addHeader('Location', Director::absoluteURL($args['location'], true)); + } + + // Clean up any pre-set headers + foreach (headers_list() as $header) { + $parts = explode(':', $header); + $name = trim($parts[0]); + + if (function_exists('header_remove')) { + header_remove($name); + } else { + header($name.':'); + } + } + + return $response; + } + + protected function respondWith() + { + return $this->respondAs(array('fields' => func_get_args())); + } + + public static $exception_noun; + + public function respondWithError($args) + { + // Make args' keys lowercase + $args = array_combine(array_map('strtolower', array_keys($args)), array_values($args)); + + // See if an exception is passed (no default) + $exception = null; + if (isset($args['exception'])) { + $exception = $args['exception']; + } + + // Get the default exception noun if set + $exceptionNoun = self::$exception_noun; + + // Add defaults + $args = array_merge(array( + // HTTP response code & description. + 'code' => 500, + 'description' => null, + // Response body. Overrides the calculation from noun + 'body' => null, + // Noun to respond with + 'noun' => ($exception && $exceptionNoun) ? new $exceptionNoun($exception) : null + ), $args); + + // Default "response" which we build into. Not returned, because the exception has it's own response + $response = new SS_HTTPResponse(); + + // If there's a formatter + if ($args['noun'] && ($formatter = RESTFormatter::get_formatter($this->request))) { + // Format the response. Revert back to default response if we got nothing. + $response = $formatter->format($args['noun'], array('*')); + if (!$response) { + $response = new SS_HTTPResponse(); + } + } elseif ($exception) { + $exception = $args['exception']; + $response->setBody($exception->getMessage()."\n"); + $response->addHeader('Content-Type', 'text/plain'); + } + + // If a specific body was provided, use that + if ($args['body']) { + $response->setBody($args['body']); + } + + // Build an exception with those details + $e = new SS_HTTPResponse_Exception($response->getBody(), $args['code'], $args['description']); + $exceptionResponse = $e->getResponse(); + + // Add user specified headers + foreach ($response->getHeaders() as $k => $v) { + $exceptionResponse->addHeader($k, $v); + } + + throw $e; + } + + public function GET($request) + { + $this->httpError(403); + } + public function POST($request) + { + $this->httpError(403); + } + public function PUT($request) + { + $this->httpError(403); + } + public function DELETE($request) + { + $this->httpError(403); + } } diff --git a/code/RESTParser.php b/code/RESTParser.php index d1aad0a..4b0887d 100644 --- a/code/RESTParser.php +++ b/code/RESTParser.php @@ -1,241 +1,290 @@ getHeader('Content-Type')); - - foreach($mimetypesWithQuality as $mimetypeWithQuality) { - $mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality); - } - return $mimetypes; - } - - private static function useful_mimetype($type) { - return $type && $type != '*/*'; - } - - /** - * Factory that returns a RESTParser subclass that parses data in a particular format based on mimetype or content - * - * TODO: This is pretty copy-pasta from RESTFormatter. Maybe consolidate this code with that? - * - * @static - * @param SS_HTTPRequest | string - $request - * @return RESTParser - an instance of a RESTParser that can do the job, or null if none can. - */ - public static function get_parser($request = null) { - // Request can be an SS_HTTPRequest object - if($request instanceof SS_HTTPRequest) { - // Try and get the mimetype from the request's Accept header - $mimetypes = self::get_content_type_mimetypes($request); - // Alternatively the type might be auto-detected from the request - $body = $request->getBody(); - } - // Request can alternatively be a string which might be a mimetype or an extension - else { - $mimetypes = array($request); $body = ''; - } - - // Filter out empty items and */* - $mimetypes = array_filter($mimetypes, array(__CLASS__, 'useful_mimetype')); - - // Now step through looking for matches on any specified mimetype or exception - $byMimeType = null; $bySignature = null; - - foreach (ClassInfo::subclassesFor(__CLASS__) as $class) { - if ($class == __CLASS__) continue; - - if($mimetypes && count(array_intersect($mimetypes, Object::get_static($class, 'mimetypes')))) { - $byMimeType = $class; - break; // Mimetypes take priority over extensions. If we get a match we're done. - } - - if($body && call_user_func(array($class, 'matches_signature'), $body)) { - $bySignature = $class; - if (!$mimetypes) break; // We're only done on a match if we don't have a mimetype to look for. - } - } - - // Mime type match gets priority over extension - if ($byMimeType) return new $byMimeType(); - if ($bySignature) return new $bySignature(); - } - - static function matches_signature($body) { /* NOP */ } - - function parseInto($request, $fields, $noun = null, $defaultType = null) { - $parsed = $this->parse($request->getBody()); - - $type = $this->getObjectType($parsed); - if (!$type) $type = $defaultType; - - if ($noun) { - if ($type && !($noun instanceof $type)) throw new Exception('Mismatched API types while parsing request', 400); - } else { - if (!$type) { throw new Exception('No API type specified', 400); } - if(!class_exists($type)) { throw new Exception('Invalid API type "'.$type.'"', 400); } - $noun = new $type(); - } - if (in_array('*', $fields)) { - $fields = array_merge($fields, Config::inst()->get($noun->class, 'default_write_fields')); - $fields = array_diff($fields, array('*')); - } - - foreach ($fields as $field) { - $required = false; - - // Check for a '!' at the end of the field, which means "required" - if ($field[strlen($field)-1] == '!') { - $field = substr($field, 0, -1); - $required = true; - } - - if ($this->getFieldExists($parsed, $field)) { - $noun->{$field} = $this->getFieldFromObject($parsed, $field); - } elseif ($required) { - throw new Exception('Required field missing'); - } - } - - return $noun; - } - - abstract protected function parse($body); - - abstract protected function getObjectType($object); - abstract protected function getFieldExists($object, $field); - abstract protected function getFieldFromObject($object, $field); +abstract class RESTParser extends Object +{ + + public static $mimetypes = array(); + + private static function get_content_type_mimetypes($request, $includeQuality = false) + { + $mimetypes = array(); + $mimetypesWithQuality = explode(',', $request->getHeader('Content-Type')); + + foreach ($mimetypesWithQuality as $mimetypeWithQuality) { + $mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality); + } + return $mimetypes; + } + + private static function useful_mimetype($type) + { + return $type && $type != '*/*'; + } + + /** + * Factory that returns a RESTParser subclass that parses data in a particular format based on mimetype or content + * + * TODO: This is pretty copy-pasta from RESTFormatter. Maybe consolidate this code with that? + * + * @static + * @param SS_HTTPRequest | string - $request + * @return RESTParser - an instance of a RESTParser that can do the job, or null if none can. + */ + public static function get_parser($request = null) + { + // Request can be an SS_HTTPRequest object + if ($request instanceof SS_HTTPRequest) { + // Try and get the mimetype from the request's Accept header + $mimetypes = self::get_content_type_mimetypes($request); + // Alternatively the type might be auto-detected from the request + $body = $request->getBody(); + } + // Request can alternatively be a string which might be a mimetype or an extension + else { + $mimetypes = array($request); + $body = ''; + } + + // Filter out empty items and */* + $mimetypes = array_filter($mimetypes, array(__CLASS__, 'useful_mimetype')); + + // Now step through looking for matches on any specified mimetype or exception + $byMimeType = null; + $bySignature = null; + + foreach (ClassInfo::subclassesFor(__CLASS__) as $class) { + if ($class == __CLASS__) { + continue; + } + + if ($mimetypes && count(array_intersect($mimetypes, Object::get_static($class, 'mimetypes')))) { + $byMimeType = $class; + break; // Mimetypes take priority over extensions. If we get a match we're done. + } + + if ($body && call_user_func(array($class, 'matches_signature'), $body)) { + $bySignature = $class; + if (!$mimetypes) { + break; + } // We're only done on a match if we don't have a mimetype to look for. + } + } + + // Mime type match gets priority over extension + if ($byMimeType) { + return new $byMimeType(); + } + if ($bySignature) { + return new $bySignature(); + } + } + + public static function matches_signature($body) + { /* NOP */ + } + + public function parseInto($request, $fields, $noun = null, $defaultType = null) + { + $parsed = $this->parse($request->getBody()); + + $type = $this->getObjectType($parsed); + if (!$type) { + $type = $defaultType; + } + + if ($noun) { + if ($type && !($noun instanceof $type)) { + throw new Exception('Mismatched API types while parsing request', 400); + } + } else { + if (!$type) { + throw new Exception('No API type specified', 400); + } + if (!class_exists($type)) { + throw new Exception('Invalid API type "'.$type.'"', 400); + } + $noun = new $type(); + } + if (in_array('*', $fields)) { + $fields = array_merge($fields, Config::inst()->get($noun->class, 'default_write_fields')); + $fields = array_diff($fields, array('*')); + } + + foreach ($fields as $field) { + $required = false; + + // Check for a '!' at the end of the field, which means "required" + if ($field[strlen($field)-1] == '!') { + $field = substr($field, 0, -1); + $required = true; + } + + if ($this->getFieldExists($parsed, $field)) { + $noun->{$field} = $this->getFieldFromObject($parsed, $field); + } elseif ($required) { + throw new Exception('Required field missing'); + } + } + + return $noun; + } + + abstract protected function parse($body); + + abstract protected function getObjectType($object); + abstract protected function getFieldExists($object, $field); + abstract protected function getFieldFromObject($object, $field); } -class RESTParser_JSON extends RESTParser { - static $mimetypes = array('application/json'); - - static function matches_signature($body) { - $body = trim($body); - $firstchar = $body[0]; - - return ($firstchar == '{' || $firstchar == '[') && json_decode($body) !== null; - } - - /** - * - * @param string $body - * @return string - JSON - */ - protected function parse($body) { - $parsedBody = json_decode($body); - if(json_last_error()) { - $this->handleError(json_last_error()); - } - return $parsedBody; - } - - /** - * Throw exception depending that correlates to the json_last_error() passed - * in. - * - * @param int $errorCode - * @throws Exception - */ - protected function handleError($errorCode) { - switch ($errorCode) { - case JSON_ERROR_DEPTH: - throw new Exception('Exceeded maximum stack depth while parsing JSON payload', 400); - break; - case JSON_ERROR_STATE_MISMATCH: - throw new Exception('Underflow or the modes mismatch when parsing JSON payload', 400); - break; - case JSON_ERROR_CTRL_CHAR: - throw new Exception('Found an unexpected control character in JSON payload', 400); - break; - case JSON_ERROR_SYNTAX: - throw new Exception('JSON payload is malformed', 400); - break; - case JSON_ERROR_UTF8: - throw new Exception('JSON payload has malformed UTF-8 characters, possibly incorrectly encoded', 400); - break; - default: - throw new Exception('Unknown json error', 400); - break; - } - } - - protected function getObjectType($object) { - $typeField = RESTFormatter_JSON::$type_attribute; - if (isset($object->$typeField)) return $object->$typeField; - } - - protected function getFieldExists($object, $field) { - return isset($object->$field); - } - - protected function getFieldFromObject($object, $field) { - return $object->$field; - } +class RESTParser_JSON extends RESTParser +{ + public static $mimetypes = array('application/json'); + + public static function matches_signature($body) + { + $body = trim($body); + $firstchar = $body[0]; + + return ($firstchar == '{' || $firstchar == '[') && json_decode($body) !== null; + } + + /** + * + * @param string $body + * @return string - JSON + */ + protected function parse($body) + { + $parsedBody = json_decode($body); + if (json_last_error()) { + $this->handleError(json_last_error()); + } + return $parsedBody; + } + + /** + * Throw exception depending that correlates to the json_last_error() passed + * in. + * + * @param int $errorCode + * @throws Exception + */ + protected function handleError($errorCode) + { + switch ($errorCode) { + case JSON_ERROR_DEPTH: + throw new Exception('Exceeded maximum stack depth while parsing JSON payload', 400); + break; + case JSON_ERROR_STATE_MISMATCH: + throw new Exception('Underflow or the modes mismatch when parsing JSON payload', 400); + break; + case JSON_ERROR_CTRL_CHAR: + throw new Exception('Found an unexpected control character in JSON payload', 400); + break; + case JSON_ERROR_SYNTAX: + throw new Exception('JSON payload is malformed', 400); + break; + case JSON_ERROR_UTF8: + throw new Exception('JSON payload has malformed UTF-8 characters, possibly incorrectly encoded', 400); + break; + default: + throw new Exception('Unknown json error', 400); + break; + } + } + + protected function getObjectType($object) + { + $typeField = RESTFormatter_JSON::$type_attribute; + if (isset($object->$typeField)) { + return $object->$typeField; + } + } + + protected function getFieldExists($object, $field) + { + return isset($object->$field); + } + + protected function getFieldFromObject($object, $field) + { + return $object->$field; + } } -class RESTParser_XML extends RESTParser { - static $mimetypes = array('text/xml'); - - static function matches_signature($body) { - if (!preg_match('/^\s*<\?xml/', $body)) return false; - - libxml_use_internal_errors(false); - $parsed = simplexml_load_string($body); - libxml_use_internal_errors(true); - - return $parsed !== null; - } - - protected function parse($body) { - $doc = new DOMDocument(); - $doc->loadXML($body); - return $doc->firstChild; - } - - protected function getObjectType($object) { - return $object->tagName; - } - - protected function getFieldExists($object, $field) { - return $object->hasAttribute($field) || $object->getElementsByTagName($field)->length > 0; - } - - protected function getFieldFromObject($object, $field) { - if ($object->hasAttribute($field)) { - return $object->getAttribute($field); - } - else { - return $object->getElementsByTagName($field)->item(0)->textContent; - } - } +class RESTParser_XML extends RESTParser +{ + public static $mimetypes = array('text/xml'); + + public static function matches_signature($body) + { + if (!preg_match('/^\s*<\?xml/', $body)) { + return false; + } + + libxml_use_internal_errors(false); + $parsed = simplexml_load_string($body); + libxml_use_internal_errors(true); + + return $parsed !== null; + } + + protected function parse($body) + { + $doc = new DOMDocument(); + $doc->loadXML($body); + return $doc->firstChild; + } + + protected function getObjectType($object) + { + return $object->tagName; + } + + protected function getFieldExists($object, $field) + { + return $object->hasAttribute($field) || $object->getElementsByTagName($field)->length > 0; + } + + protected function getFieldFromObject($object, $field) + { + if ($object->hasAttribute($field)) { + return $object->getAttribute($field); + } else { + return $object->getElementsByTagName($field)->item(0)->textContent; + } + } } -class RESTParser_FormData extends RESTParser { - static $mimetypes = array('application/x-www-form-urlencoded', 'multipart/form-data'); - - static function matches_signature($body) { - // Only use this parser if mime-type matches - return false; - } - - protected function parse($body) { - return $_REQUEST; - } - - protected function getObjectType($object) { - if (isset($object['type'])) return $object['$type']; - } - - protected function getFieldExists($object, $field) { - return isset($object[$field]); - } - - protected function getFieldFromObject($object, $field) { - return $object[$field]; - } +class RESTParser_FormData extends RESTParser +{ + public static $mimetypes = array('application/x-www-form-urlencoded', 'multipart/form-data'); + + public static function matches_signature($body) + { + // Only use this parser if mime-type matches + return false; + } + + protected function parse($body) + { + return $_REQUEST; + } + + protected function getObjectType($object) + { + if (isset($object['type'])) { + return $object['$type']; + } + } + + protected function getFieldExists($object, $field) + { + return isset($object[$field]); + } + + protected function getFieldFromObject($object, $field) + { + return $object[$field]; + } } diff --git a/code/RESTRoot.php b/code/RESTRoot.php index 20dd862..371e674 100644 --- a/code/RESTRoot.php +++ b/code/RESTRoot.php @@ -4,48 +4,58 @@ // restassured/_config because earlier _configs might call the static methods require_once dirname(__FILE__)."/RESTNoun.php"; -class RESTRoot extends ViewableData { - use RESTNoun; - - static protected $registered_handlers = array(); - - static function register($name, $collectionClass) { - self::$registered_handlers[$name] = $collectionClass; - } - - static function unregister($name) { - unset(self::$registered_handlers[$name]); - } - - static function get_registered() { - return self::$registered_handlers; - } - - function __get($name) { - if (isset(self::$registered_handlers[$name])) { - $collectionClass = self::$registered_handlers[$name]; - $res = new $collectionClass(); - $res->parent = $this; - $res->linkFragment = $name; - return $res; - } - return parent::__get($name); - } - - function Link() { - return RESTASSURED_ROOT; - } - - function LinkFor($item) { - if ($item->parent !== $this) user_error('Tried to get link for noun that is not a root item', E_USER_ERROR); - return Controller::join_links($this->Link(), $item->linkFragment); - } +class RESTRoot extends ViewableData +{ + use RESTNoun; + + protected static $registered_handlers = array(); + + public static function register($name, $collectionClass) + { + self::$registered_handlers[$name] = $collectionClass; + } + + public static function unregister($name) + { + unset(self::$registered_handlers[$name]); + } + + public static function get_registered() + { + return self::$registered_handlers; + } + + public function __get($name) + { + if (isset(self::$registered_handlers[$name])) { + $collectionClass = self::$registered_handlers[$name]; + $res = new $collectionClass(); + $res->parent = $this; + $res->linkFragment = $name; + return $res; + } + return parent::__get($name); + } + + public function Link() + { + return RESTASSURED_ROOT; + } + + public function LinkFor($item) + { + if ($item->parent !== $this) { + user_error('Tried to get link for noun that is not a root item', E_USER_ERROR); + } + return Controller::join_links($this->Link(), $item->linkFragment); + } } -class RESTRoot_Handler extends RESTNoun_Handler { +class RESTRoot_Handler extends RESTNoun_Handler +{ - protected function respondWith($fields = array()) { - // Not implemented yet. Eventually probably want to have some sort of discovery support - } - -} \ No newline at end of file + protected function respondWith($fields = array()) + { + // Not implemented yet. Eventually probably want to have some sort of discovery support + } +} diff --git a/code/RESTRouter.php b/code/RESTRouter.php index 6e775eb..500a811 100644 --- a/code/RESTRouter.php +++ b/code/RESTRouter.php @@ -1,89 +1,105 @@ pushCurrent(); - - $this->urlParams = $request->allParams(); - $this->request = $request; - $this->response = new SS_HTTPResponse(); - - $this->extend('onBeforeInit'); - - // Init - $this->baseInitCalled = false; - $this->init(); - if(!$this->baseInitCalled) user_error("init() method on class '$this->class' doesn't call Controller::init(). Make sure that you have parent::init() included.", E_USER_WARNING); - - $this->extend('onAfterInit'); - - // If we had a redirection or something, halt processing. - if(!$this->response->isFinished()) { - $this->response = $this->routeRequest($request); - } - - $this->popCurrent(); - return $this->response; - } - - function routeRequest(SS_HTTPRequest $request) { - // Handle the routing - - $noun = singleton('RESTRoot'); - - while (!$request->allParsed()) { - $matched = false; - - if($params = $request->match('$Next!', true)) { - $matched = true; - $next = $params['Next']; - - try { - if (method_exists($noun, 'getItem')) $noun = $noun->getItem($next); - else $noun = $noun->$next; - } catch (Exception $e) { - if ($e instanceof SS_HTTPResponse_Exception) { - throw $e; - } elseif ($e instanceof RESTException) { - $handler = $this->getHandler($noun); - $handler->respondWithError(array('code' => $e->getCode(), 'exception' => $e)); - } else { - $handler = $this->getHandler($noun); - $handler->respondWithError(array('code' => 500, 'exception' => $e)); - } - } - - if (!$noun) $this->httpError(404); - } - - if (!$matched) $this->httpError(404); - } - - // Find the handler and call - - $handler = $this->getHandler($noun); - return $handler->handleRequest($request); - } - - function getHandler($noun) { - $ancestry = array_reverse(ClassInfo::ancestry($noun->class)); - foreach ($ancestry as $class) { - $class = $class . '_Handler'; - if (ClassInfo::exists($class)) return new $class($noun); - } - - user_error("Couldn't find a handler for REST Noun ".$noun, E_USER_ERROR); - } - -} \ No newline at end of file +class RESTRouter extends Controller +{ + + protected $context; + + /** + * We don't want Controller's handling of URLs, but we do need this to appear to go through the usual + * startup procedure (push controller, call init, etc). + * + * So this is a duplication of Controller#handleRequest, except for the ending stanza + */ + public function handleRequest(SS_HTTPRequest $request, DataModel $model) + { + if (!$request) { + user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR); + } + + $this->pushCurrent(); + + $this->urlParams = $request->allParams(); + $this->request = $request; + $this->response = new SS_HTTPResponse(); + + $this->extend('onBeforeInit'); + + // Init + $this->baseInitCalled = false; + $this->init(); + if (!$this->baseInitCalled) { + user_error("init() method on class '$this->class' doesn't call Controller::init(). Make sure that you have parent::init() included.", E_USER_WARNING); + } + + $this->extend('onAfterInit'); + + // If we had a redirection or something, halt processing. + if (!$this->response->isFinished()) { + $this->response = $this->routeRequest($request); + } + + $this->popCurrent(); + return $this->response; + } + + public function routeRequest(SS_HTTPRequest $request) + { + // Handle the routing + + $noun = singleton('RESTRoot'); + + while (!$request->allParsed()) { + $matched = false; + + if ($params = $request->match('$Next!', true)) { + $matched = true; + $next = $params['Next']; + + try { + if (method_exists($noun, 'getItem')) { + $noun = $noun->getItem($next); + } else { + $noun = $noun->$next; + } + } catch (Exception $e) { + if ($e instanceof SS_HTTPResponse_Exception) { + throw $e; + } elseif ($e instanceof RESTException) { + $handler = $this->getHandler($noun); + $handler->respondWithError(array('code' => $e->getCode(), 'exception' => $e)); + } else { + $handler = $this->getHandler($noun); + $handler->respondWithError(array('code' => 500, 'exception' => $e)); + } + } + + if (!$noun) { + $this->httpError(404); + } + } + + if (!$matched) { + $this->httpError(404); + } + } + + // Find the handler and call + + $handler = $this->getHandler($noun); + return $handler->handleRequest($request); + } + + public function getHandler($noun) + { + $ancestry = array_reverse(ClassInfo::ancestry($noun->class)); + foreach ($ancestry as $class) { + $class = $class . '_Handler'; + if (ClassInfo::exists($class)) { + return new $class($noun); + } + } + + user_error("Couldn't find a handler for REST Noun ".$noun, E_USER_ERROR); + } +}