/** * Adds a component to the application. A component is a subset of the application * functionality. You must provide a interface or class name or a list of * interfaces and class name that are part of the component. * * The Component ID should be a human-readable name for the component, one or two * words without spaces and URL-safe. * * @history * 2014.04.15: * (AT) Initial implementation * * @version 2014.04.15 * @author (AT) Alberto Trevino, Brigham Young Univ. <*****@*****.**> * * @param string $component_id * A short, URI-safe descriptive name for the component * @param string $component_name * A human-readable name for the component * @param mixed $class_list * String with fully-qualified class name or array of class names * @param string $description * Description for the functionality of the component (optional); if * not specified, description will be taken from class documentation * block * @throws \Cougar\Exceptions\Exception */ public function addComponent($component_id, $component_name, $class_list, $description = null) { // Make sure the component has not been defined already if (array_key_exists($component_id, $this->components)) { throw new Exception("Component \"" . $component_id . "\" has already been defined"); } // Create a new component and set the ID and name $component = new Component(); $component->componentId = $component_id; $component->name = $component_name; // See if we have a description if ($description) { $component->description = $description; } // See if the class list is a string with a single class if (!is_array($class_list)) { $class_list = array((string) $class_list); } // Go through the list of classes foreach ($class_list as $class) { // Load the method_info for this class $class_annotations = Annotations::extractFromObjectWithInheritance($class); // See if we need to create the description from the class doc block if (!$description && array_key_exists("_comment", $class_annotations->class)) { // See if we already have a description in the component if ($component->description) { $component->description .= "\n\n" . $class_annotations->class["_comment"]; } else { $component->description = $class_annotations->class["_comment"]; } } // Go through each method foreach ($class_annotations->methods as $annotations) { // Extract the method information $method_info = $this->parseMethodAnnotations($annotations); // See if this method has a Path annotation; if it doesn't have // one then the method is not part of the REST API if (!$method_info["path"]) { // Go to the next method continue; } // Extract the resource $resource = $this->extractResource($method_info); // See if this resource already exists if (!array_key_exists($resource->name, $this->resources)) { // Add the resource to the resource lists $this->resources[$resource->name] = $resource; $component->resources[] = $resource; } else { // Point the resource variable to the existing resource $resource = $this->resources[$resource->name]; } // Get the action on the resource $action = $this->extractAction($method_info); // Figure out an ID for this action $count = 0; foreach ($resource->actions as $tmp_action) { if ($tmp_action->name == $action->name) { $count++; } } if ($count) { $action->actionId = $action->name . "." . $count; } else { $action->actionId = $action->name; } // Add the action to the resource's action list $resource->actions[] = $action; } } // Add the component $this->components[] = $component; }
/** * @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; }