/** * Extracts the resource from a method's method_info * * @history * 2014.04.29: * (AT) Initial implementation * 2014.05.29: * (AT) Added short description to the resource * * @version 2014.05.29 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @param array $method_info * Method information * @return \Cougar\RestService\Models\Resource Resource object */ protected function extractResource(array $method_info) { // Initialize the resource variable $resource = null; if ($method_info["resource"]) { // See if we have a class if ($method_info["resource"]["class"]) { // Create the resource $resource = new Resource(); $resource->class = $method_info["resource"]["class"]; // See if we have an alternate name if ($method_info["resource"]["name"]) { // Set the alternate resource name $resource->name = $method_info["resource"]["name"]; } else { // Set the resource name from the class name $resource->name = $this->generateResourceName($method_info["resource"]["class"]); } } } // See if we had a resource if (!$resource) { // See if we have a return annotation if ($method_info["return"]) { // See if the return type is a primitive if (!$this->isPrimitive($method_info["return"]["type"])) { // Create the resource from the return type $resource = new Resource(); $resource->class = $method_info["return"]["type"]; $resource->name = $this->generateResourceName($resource->class); } } else { if (count($method_info["param"]) == 1 && $method_info["param"][0]["type"] && !$this->isPrimitive($method_info["param"][0]["type"])) { // Get the resource name from the first and only parameter $resource = new Resource(); $resource->class = $method_info["param"][0]["type"]; $resource->name = $this->generateResourceName($resource->class); } } } // If we still don't have a resource, create a new unnamed one if (!$resource) { $resource = new Resource(); $resource->name = "Resource-" . (count($this->resources) + 1); } // Set the Resource ID from the name $resource->resourceId = $resource->name; // Set the short description from the class name if ($resource->class) { try { $class_annotations = Annotations::extractFromObject($resource->class); foreach ($class_annotations->class as $annotation) { if ($annotation->name == "_comment") { // Extract the first sentence $first_period_pos = mb_strpos($annotation->value, "."); if ($first_period_pos) { // The description is the first sentence $resource->shortDescription = substr($annotation->value, 0, $first_period_pos); } else { // The entire comment is the description $resource->shortDescription = $annotation->value; } break; } } } catch (\Exception $e) { // Ignore the error } } # Return the resource return $resource; }
/** * @covers Cougar\\Util\\Annotations::extractFromObject */ public function testInterfaceAnnotationsFromObjectWithInheritance() { # Mock the cache $local_cache = $this->getMock("\\Cougar\\Cache\\Cache"); $local_cache->expects($this->any())->method("get")->will($this->returnValue(false)); $local_cache->expects($this->any())->method("set")->will($this->returnValue(false)); Annotations::$cache = $local_cache; $annotations = Annotations::extractFromObjectWithInheritance(__NAMESPACE__ . "\\BasicClassFromInterface"); $this->assertInstanceOf("Cougar\\Util\\ClassAnnotations", $annotations); $this->assertCount(2, $annotations->class); $this->assertEquals(new Annotation("Interface", "Interface annotation"), $annotations->class[0]); $this->assertCount(2, $annotations->class); $this->assertEquals(new Annotation("Class", "Class annotation"), $annotations->class[1]); $this->assertCount(0, $annotations->properties); $this->assertCount(1, $annotations->methods); $this->assertArrayHasKey("doSomething", $annotations->methods); $this->assertCount(4, $annotations->methods["doSomething"]); $this->assertEquals(new Annotation("_comment", "This is a method in an interface"), $annotations->methods["doSomething"][0]); $this->assertEquals(new Annotation("param", "int \$number"), $annotations->methods["doSomething"][1]); $this->assertEquals(new Annotation("_comment", "This is the description in the implementation"), $annotations->methods["doSomething"][2]); $this->assertEquals(new Annotation("param", "int \$number Some number"), $annotations->methods["doSomething"][3]); }
/** * Extracts the annotation for the class and parses them into the * __-prefixed protected properties. * * @history * 2013.09.30: * (AT) Initial release * 2014.02.26: * (AT) Extract annotations with extractFromObjectWithInheritance() * 2014.03.05: * (AT) Don't clobber cached annotations when loading parsed annotations * from cache * (AT) Switch from using __defaultValues to __previousValues * 2014.08.06: * (AT) Turn the execution cache into a proper memory cache * * @version 2014.08.06 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @param mixed $object * Assoc. array or object with initial values * @param string $view * Set the given view once values are loaded * @param bool $strict * Whether to perform strict property checking (on by default) * @throws Exception * @throws BadRequestException */ public function __construct($object = null, $view = null, $strict = true) { # Store the value of the requested view (avoid clobbering later) $requested_view = $view; # Get a local cache and a memory cache # TODO: Set through static property(?) $local_cache = CacheFactory::getLocalCache(); $execution_cache = CacheFactory::getMemoryCache(); # Create our cache keys $class = get_class($this) . ".Model"; $cache_key = Annotations::$annotationsCachePrefix . "." . $class; # See if the execution cache has the object properties $parsed_annotations = $execution_cache->get($cache_key); if (!$parsed_annotations) { # Get the annotations $this->__annotations = Annotations::extractFromObjectWithInheritance($this, array(), true, false); # See if the annotations came from the cache $parsed_annotations = false; if ($this->__annotations->cached) { $parsed_annotations = $local_cache->get($cache_key); } } # See if we have pre-parsed annotations if ($parsed_annotations === false) { # Go through the class annotations $view_list = array("__default__"); foreach ($this->__annotations->class as $annotation) { switch ($annotation->name) { case "CaseInsensitive": $this->__caseInsensitive = true; break; case "Views": # See which views are defined $views = preg_split('/\\s+/u', $annotation->value, null, PREG_SPLIT_NO_EMPTY); # Create the views (if we have any) foreach ($views as $view) { $this->__views[$view] = $this->__views["__default__"]; $view_list[] = $view; } break; } } # Go through the public properties of the object foreach (array_keys($this->__annotations->properties) as $property_name) { # Add the property to the list of properties $this->__properties[] = $property_name; # Set the default property options $this->__type[$property_name] = "string"; $this->__readOnly[$property_name] = false; $this->__null[$property_name] = true; $this->__regex[$property_name] = array(); $this->__alias[$property_name] = $property_name; # See if the properties are case-insensitive if ($this->__caseInsensitive) { # Store the lowercase property name as an alias $this->__alias[strtolower($property_name)] = $property_name; } # Set the view-based values foreach ($view_list as $view) { $this->__views[$view]["optional"][$property_name] = false; $this->__views[$view]["visible"][$property_name] = true; $this->__views[$view]["exportAlias"][$property_name] = $property_name; } # Go through the annotations foreach ($this->__annotations->properties[$property_name] as $annotation) { switch ($annotation->name) { case "Alias": case "Column": $this->__alias[$annotation->value] = $property_name; if ($this->__caseInsensitive) { $this->__alias[strtolower($annotation->value)] = $property_name; } break; case "NotNull": $this->__null[$property_name] = false; break; case "Regex": $this->__regex[$property_name][] = $annotation->value; break; case "Optional": # Set the option in all views foreach ($view_list as $view) { $this->__views[$view]["optional"][$property_name] = true; } break; case "DateTimeFormat": $this->__dateTimeFormat[$property_name] = $annotation->value; break; case "View": # Separate the values $view_values = preg_split('/\\s+/u', $annotation->value); # Extract the view (first value) $view = array_shift($view_values); # Make sure the view exists if (!array_key_exists($view, $this->__views)) { throw new Exception($property_name . " property defines \"" . $view . "\" but the view does not exist."); } # Go through the rest of the options $export_alias_set = false; foreach ($view_values as $index => $value) { switch (strtolower($value)) { case "hidden": $this->__views[$view]["visible"][$property_name] = false; break; case "optional": $this->__views[$view]["optional"][$property_name] = true; break; default: # Add the real value (not lowercase) as # the export alias and as an alias if (!$export_alias_set) { $this->__views[$view]["exportAlias"][$property_name] = $view_values[$index]; $this->__alias[$view_values[$index]] = $property_name; if ($this->__caseInsensitive) { $this->__alias[strtolower($view_values[$index])] = $property_name; } } $export_alias_set = true; break; } } break; case "var": # Separate the variable name from the comment $var_values = preg_split('/\\s+/u', $annotation->value); switch ($var_values[0]) { case "string": case "": # Type is already set to string break; case "int": case "integer": $this->__type[$property_name] = "int"; break; case "float": case "double": $this->__type[$property_name] = "float"; break; case "bool": case "boolean": $this->__type[$property_name] = "bool"; break; case "DateTime": $this->__type[$property_name] = "DateTime"; if (!array_key_exists($property_name, $this->__dateTimeFormat)) { $this->__dateTimeFormat[$property_name] = ""; } break; default: $this->__type[$property_name] = $var_values[0]; } break; } } } # Get the default values foreach ($this->__properties as $property) { $this->__defaultValues[$property] = $this->{$property}; } # Store the record properties in the caches $parsed_annotations = array("annotations" => $this->__annotations, "properties" => $this->__properties, "type" => $this->__type, "readOnly" => $this->__readOnly, "null" => $this->__null, "dateTimeFormat" => $this->__dateTimeFormat, "regex" => $this->__regex, "alias" => $this->__alias, "caseInsensitive" => $this->__caseInsensitive, "view" => $this->__views, "defaultValues" => $this->__defaultValues); $execution_cache->set($cache_key, $parsed_annotations); $local_cache->set($cache_key, $parsed_annotations, Annotations::$cacheTime); } else { # Make sure we don't clobber any previous annotations # (otherwise we may lose the cached setting) if (!$this->__annotations) { $this->__annotations = $parsed_annotations["annotations"]; } # Restore the property values $this->__properties = $parsed_annotations["properties"]; $this->__type = $parsed_annotations["type"]; $this->__readOnly = $parsed_annotations["readOnly"]; $this->__null = $parsed_annotations["null"]; $this->__dateTimeFormat = $parsed_annotations["dateTimeFormat"]; $this->__regex = $parsed_annotations["regex"]; $this->__alias = $parsed_annotations["alias"]; $this->__caseInsensitive = $parsed_annotations["caseInsensitive"]; $this->__views = $parsed_annotations["view"]; $this->__defaultValues = $parsed_annotations["defaultValues"]; } # Set the previous values from the default values $this->__previousValues = $this->__defaultValues; # See if we have an incoming object or array if (is_array($object) || is_object($object)) { # Load the incoming values $this->__import($object, $strict); } else { if (!is_null($object)) { throw new BadRequestException("Casting from object requires an object or array"); } } # Set the desired view if ($requested_view) { $this->__setView($requested_view); } else { # Point the protected properties to the values in the default view $this->__exportAlias =& $this->__views["__default__"]["exportAlias"]; $this->__optional =& $this->__views["__default__"]["optional"]; $this->__visible =& $this->__views["__default__"]["visible"]; } }
/** * Extracts the annotations from the class, property and method blocks and * stores them in the protected __annotations property. * * @history * 2013.09.30: * (AT) Initial release * 2014.02.26: * (AT) Extract annotations with extractFromObjectWithInheritance() * * @version 2014.02.26 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> */ public function __construct() { $this->__annotations = Annotations::extractFromObjectWithInheritance($this, array(), true, false); }
/** * Binds all the services in the given object. This call can be made as * many times as as necessary to bind all necessary services. * * @history * 2013.09.30: * (AT) Initial release * 2013.10.16: * (AT) Fix clobbering issue where calling the method a second time * deletes previous bindings * 2014.02.26: * (AT) Extract annotations using extractFromObjectWithInheritance() * method and enable inheritance from interfaces * 2014.03.10: * (AT) Add ^/ to the path regex to match explicitly on the beginning of * the path * 2014.03.12: * (AT) Require a URI parameter to contain at least one character * (AT) Add the number of URI parameters and literals to the binding * 2014.03.14: * (AT) Fix matching of root paths (those are simply /) * (AT) Add $ to end of regex to be more exact on path matching * (AT) Make sure to set the parameter name when the parameter contains a * regular expression * 2014.08.05: * (AT) Internally convert the Returns and Accepts annotations to * lowercase to improve and simplify mimetype detection * * @version 2014.08.05 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @param object $object_reference * Reference to the object that will be bound * @throws \Cougar\Exceptions\Exception; */ public function bindFromObject(&$object_reference) { # Make sure this is an object if (!is_object($object_reference)) { throw new Exception("Object reference must be an object"); } # Get the class name $class = get_class($object_reference); # Skip if this class is already in the object list if (array_key_exists($class, $this->objects)) { throw new Exception("You have attempted to bind an object twice " . "or bind two objects of the same class; please verify your " . "object bindings"); } # Create our own cache key $cache_key = Annotations::$annotationsCachePrefix . $class . ".AnnotatedRestService.Bindings"; # Get the annotations $annotations = Annotations::extractFromObjectWithInheritance($object_reference, array(), true, true); # See if we have pre-parsed bindings $bindings = false; if ($annotations->cached) { $bindings = $this->localCache->get($cache_key); } # See if we need to extract the bindings from the annotations if ($bindings === false) { # Start a blank bindings list $bindings = array(); # Go through the object's methods foreach ($annotations->methods as $method => $annotations) { # Create the binding and initialize the paths and methods array $binding = new Binding(); $paths = array(); # Add the class and method information about the binding $binding->object = $class; $binding->method = $method; $binding->http_methods = array("GET", "POST", "PUT", "DELETE"); # Extract the property's annotations foreach ($annotations as $annotation) { switch ($annotation->name) { case "Path": $paths[] = $annotation->value; break; case "Methods": $binding->http_methods = preg_split('/\\s+/u', mb_strtoupper($annotation->value)); break; case "Accepts": $binding->accepts = mb_strtolower($annotation->value); break; case "Returns": $binding->returns = mb_strtolower($annotation->value); break; case "XmlRootElement": case "RootElement": $binding->xmlRootElement = $annotation->value; break; case "XmlObjectName": case "ObjectName": $binding->xmlObjectName = $annotation->value; break; case "XmlObjectList": $binding->xmlObjectList = $annotation->value; break; case "XSD": $binding->xsd = file_get_contents($annotation->value, true); break; case "XSL": $binding->xsl = file_get_contents($annotation->value, true); break; case "UriArray": $parameter = new Parameter(); $parameter->source = "URI"; $parameter->index = 0; $parameter->array = true; $binding->parameters[$annotation->value] = $parameter; break; case "GetArray": $parameter = new Parameter(); $parameter->source = "GET"; $parameter->index = 0; $parameter->array = true; $binding->parameters[$annotation->value] = $parameter; break; case "GetValue": # Define the new entry $parameter = new Parameter(); # Split the values at word boundaries $values = preg_split('/\\s+/u', $annotation->value, 3); # See how many values we have switch (count($values)) { case 3: # type variable_name method_parameter_name $param_name = $values[2]; $parameter->source = "GET"; $parameter->index = $values[1]; $parameter->type = $values[0]; break; case 2: # type get_variable_name $param_name = $values[1]; $parameter->source = "GET"; $parameter->index = $values[1]; $parameter->type = $values[0]; break; case 1: # get_variable_name $param_name = $values[0]; $parameter->source = "GET"; $parameter->index = $values[0]; $parameter->type = "string"; break; default: throw new InvalidAnnotationException("Invalid GetValue: " . $annotation->value); } # Add the parameter $binding->parameters[$param_name] = $parameter; break; case "GetQuery": # Define the new entry $parameter = new Parameter(); # Split the values at word boundaries $values = preg_split('/\\s+/u', $annotation->value, 3); # See how many values we have switch (count($values)) { case 1: # get_variable_name $param_name = $values[0]; $parameter->source = "QUERY"; $parameter->type = "array"; break; default: throw new InvalidAnnotationException("Invalid GetQuery: " . $annotation->value); } # Add the parameter $binding->parameters[$param_name] = $parameter; break; case "PostArray": $parameter = new Parameter(); $parameter->source = "POST"; $parameter->index = 0; $parameter->array = true; $binding->parameters[$annotation->value] = $parameter; break; case "PostValue": # Define the new entry $parameter = new Parameter(); # Split the values at word boundaries $values = preg_split('/\\s+/u', $annotation->value, 3); # See how many values we have switch (count($values)) { case 3: # type variable_name method_parameter_name $param_name = $values[2]; $parameter->source = "POST"; $parameter->index = $values[1]; $parameter->type = $values[0]; break; case 2: # type get_variable_name $param_name = $values[1]; $parameter->source = "POST"; $parameter->index = $values[1]; $parameter->type = $values[0]; break; case 1: # get_variable_name $param_name = $values[0]; $parameter->source = "POST"; $parameter->index = $values[0]; $parameter->type = "string"; break; default: throw new InvalidAnnotationException("Invalid PostValue: " . $annotation->value); } # Add the parameter $binding->parameters[$param_name] = $parameter; break; case "Body": # Define the new entry $parameter = new Parameter(); # Split the values at word boundaries $values = preg_split('/\\s+/u', $annotation->value, 2); # See how many values we have switch (count($values)) { case 2: # parameter_name type $param_name = $values[0]; $parameter->source = "BODY"; $parameter->type = $values[1]; break; case 1: $param_name = $values[0]; $parameter->source = "BODY"; break; default: throw new InvalidAnnotationException("Invalid Body: " . $annotations->value); } # Add the parameter $binding->parameters[$param_name] = $parameter; break; case "Authentication": if (mb_strtolower($annotation->value == "required")) { $binding->authentication = "required"; } else { $binding->authentication = "optional"; } break; } } # Go through each path foreach ($paths as $str_path) { # Clone the binding $real_binding = clone $binding; # Remote leading and trailing slashes on the path $str_path = preg_replace(':^/|/$:', "", $str_path); # Separate the path elements by the backslash $path = explode("/", $str_path); # See if the first element is blank (usually the case) if ($path[0] === "") { # Get rid of the element array_shift($path); } # Check for root path (a simple / for the path) if (count($path) == 1 && $path[0] === "") { # The number of URI parameters is 0 $real_binding->pathArgumentCount = 0; } else { # Add the number of URI parameters to the binding $real_binding->pathArgumentCount = count($path); } # Go through each part of the path foreach ($path as $index => &$subpath) { # Split the subpath into its parts $subpath_parts = explode(":", $subpath, 4); $subpath_count = count($subpath_parts); # See if this is a literal argument if ($subpath_count < 2) { # See if we had a value (useful for root path) # Increase the literal argument count $real_binding->literalPathArgumentCount++; # Go on to the next argument continue; } # Define the new parameter, its name and regex # expression, and add the number of path parameters $parameter = new Parameter(); $param_name = ""; if (mb_substr($subpath, -1) == "+") { $param_regex = ".*"; # Declare the number of arguments to be 1000 $real_binding->pathArgumentCount = 1000; } else { $param_regex = "[^/]+"; } # See how many parts we have switch ($subpath_count) { case 4: # :param_name:type:regex $param_name = $subpath_parts[1]; $parameter->source = "URI"; $parameter->index = $index; $parameter->type = $subpath_parts[2]; $param_regex = $subpath_parts[3]; break; case 3: # :param_name:type $param_name = $subpath_parts[1]; $parameter->source = "URI"; $parameter->index = $index; $parameter->type = $subpath_parts[2]; break; case 2: # :param_name $param_name = $subpath_parts[1]; $parameter->source = "URI"; $parameter->index = $index; $parameter->type = "string"; break; } # See if the have a + at the end of this parameter if (mb_substr($param_name, -1) == "+") { # Get rid of the + $param_name = mb_substr($param_name, 0, -1); # Set the _array flag $parameter->array = true; } # Add the parameter $real_binding->parameters[$param_name] = $parameter; # Replace the value of the parameter with its regular # expression $subpath = $param_regex; } # Reconstruct the path from the new regex values $new_path = "^/" . implode("/", $path) . "\$"; # Add the binding $bindings[$new_path][] = $real_binding; } } # Store the parsed bindings $this->localCache->set($cache_key, $bindings, Annotations::$cacheTime); } # Add the bindings to our bindings list $this->bindings = array_merge($this->bindings, $bindings); # Store the object reference $this->objects[$class] = $object_reference; }