Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/** $Id: IntrospectionHelper.php 144 2007-02-05 15:19:00Z hans $** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.** This software consists of voluntary contributions made by many individuals* and is licensed under the LGPL. For more information please see* <http://phing.info>.*/include_once 'phing/types/Reference.php';include_once 'phing/types/Path.php';include_once 'phing/util/StringHelper.php';/*** Helper class that collects the methods that a task or nested element* holds to set attributes, create nested elements or hold PCDATA* elements.**<ul>* <li><strong>SMART-UP INLINE DOCS</strong></li>* <li><strong>POLISH-UP THIS CLASS</strong></li>*</ul>** @author Andreas Aderhold <andi@binarycloud.com>* @author Hans Lellelid <hans@xmpl.org>* @copyright © 2001,2002 THYRELL. All rights reserved* @version $Revision: 1.19 $* @package phing*/class IntrospectionHelper {/*** Holds the attribute setter methods.** @var array string[]*/private $attributeSetters = array();/*** Holds methods to create nested elements.** @var array string[]*/private $nestedCreators = array();/*** Holds methods to store configured nested elements.** @var array string[]*/private $nestedStorers = array();/*** Map from attribute names to nested types.*/private $nestedTypes = array();/*** New idea in phing: any class can register certain* keys -- e.g. "task.current_file" -- which can be used in* task attributes, if supported. In the build XML these* are referred to like this:* <regexp pattern="\n" replace="%{task.current_file}"/>* In the type/task a listener method must be defined:* function setListeningReplace($slot) {}* @var array string[]*/private $slotListeners = array();/*** The method to add PCDATA stuff.** @var string Method name of the addText (redundant?) method, if class supports it :)*/private $methodAddText = null;/*** The Class that's been introspected.** @var object* @access private*/private $bean;/*** The cache of IntrospectionHelper classes instantiated by getHelper().* @var array IntrospectionHelpers[]*/private static $helpers = array();/*** Factory method for helper objects.** @param string $class The class to create a Helper for*/public static function getHelper($class) {if (!isset(self::$helpers[$class])) {self::$helpers[$class] = new IntrospectionHelper($class);}return self::$helpers[$class];}/*** This function constructs a new introspection helper for a specific class.** This method loads all methods for the specified class and categorizes them* as setters, creators, slot listeners, etc. This way, the setAttribue() doesn't* need to perform any introspection -- either the requested attribute setter/creator* exists or it does not & a BuildException is thrown.** @param string $bean The classname for this IH.*/function __construct($class) {$this->bean = new ReflectionClass($class);//$methods = get_class_methods($bean);foreach($this->bean->getMethods() as $method) {if ($method->isPublic()) {// We're going to keep case-insensitive method names// for as long as we're allowed :) It makes it much// easier to map XML attributes to PHP class method names.$name = strtolower($method->getName());// There are a few "reserved" names that might look like attribute setters// but should actually just be skipped. (Note: this means you can't ever// have an attribute named "location" or "tasktype" or a nested element named "task".)if ($name === "setlocation" || $name === "settasktype" || $name === "addtask") {continue;}if ($name === "addtext") {$this->methodAddText = $method;} elseif (strpos($name, "setlistening") === 0) {// Phing supports something unique called "RegisterSlots"// These are dynamic values that use a basic slot system so that// classes can register to listen to specific slots, and the value// will always be grabbed from the slot (and never set in the project// component). This is useful for things like tracking the current// file being processed by a filter (e.g. AppendTask sets an append.current_file// slot, which can be ready by the XSLTParam type.)if (count($method->getParameters()) !== 1) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");}$this->slotListeners[$name] = $method;} elseif (strpos($name, "set") === 0) {// A standard attribute setter.if (count($method->getParameters()) !== 1) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");}$this->attributeSetters[$name] = $method;} elseif (strpos($name, "create") === 0) {if (count($method->getParameters()) > 0) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() may not take any parameters.");}// Because PHP doesn't support return types, we are going to do// two things here to guess return type:// 1) parse comments for an explicit value// 2) if that fails, assume that the part of the method after "create"// is the name of the return type (in many cases it is not)// This isn't super important -- i.e. we're not instantaiting classes// based on this information. It's more just so that IntrospectionHelper// can keep track of all the nested types -- and provide more helpful// exception messages, etc.preg_match('/@return[\s]+([\w]+)/', $method->getDocComment(), $matches);if (!empty($matches[1]) && class_exists($matches[1], false)) {$this->nestedTypes[$name] = $matches[1];} else {// assume that method createEquals() creates object of type "Equals"// (that example would be false, of course)$this->nestedTypes[$name] = $this->getPropertyName($name, "create");}$this->nestedCreators[$name] = $method;} elseif (strpos($name, "addconfigured") === 0) {// *must* use class hints if using addConfigured ...// 1 param only$params = $method->getParameters();if (count($params) < 1) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");}if (count($params) > 1) {$this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");}$classname = null;if (($hint = $params[0]->getClass()) !== null) {$classname = $hint->getName();}if ($classname === null) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");}$this->nestedTypes[$name] = $classname;$this->nestedStorers[$name] = $method;} elseif (strpos($name, "add") === 0) {// *must* use class hints if using add ...// 1 param only$params = $method->getParameters();if (count($params) < 1) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");}if (count($params) > 1) {$this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");}$classname = null;if (($hint = $params[0]->getClass()) !== null) {$classname = $hint->getName();}// we don't use the classname here, but we need to make sure it exists before// we later try to instantiate a non-existant classif ($classname === null) {throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");}$this->nestedCreators[$name] = $method;}} // if $method->isPublic()} // foreach}/** Sets the named attribute. */function setAttribute(Project $project, $element, $attributeName, &$value) {// we want to check whether the value we are setting looks like// a slot-listener variable: %{task.current_file}//// slot-listener variables are not like properties, in that they cannot be mixed with// other text values. The reason for this disparity is that properties are only// set when first constructing objects from XML, whereas slot-listeners are always dynamic.//// This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose// typing.if (StringHelper::isSlotVar($value)) {$as = "setlistening" . strtolower($attributeName);if (!isset($this->slotListeners[$as])) {$msg = $this->getElementName($project, $element) . " doesn't support a slot-listening '$attributeName' attribute.";throw new BuildException($msg);}$method = $this->slotListeners[$as];$key = StringHelper::slotVar($value);$value = Register::getSlot($key); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue())} else {// Traditional value options$as = "set".strtolower($attributeName);if (!isset($this->attributeSetters[$as])) {$msg = $this->getElementName($project, $element) . " doesn't support the '$attributeName' attribute.";throw new BuildException($msg);}$method = $this->attributeSetters[$as];if ($as == "setrefid") {$value = new Reference($value);} else {// decode any html entities in string$value = html_entity_decode($value);// value is a string representation of a boolean type,// convert it to primitiveif (StringHelper::isBoolean($value)) {$value = StringHelper::booleanValue($value);}// does method expect a PhingFile object? if so, then// pass a project-relative file.$params = $method->getParameters();$classname = null;if (($hint = $params[0]->getClass()) !== null) {$classname = $hint->getName();}// there should only be one param; we'll just assume ....if ($classname !== null) {switch(strtolower($classname)) {case "phingfile":$value = $project->resolveFile($value);break;case "path":$value = new Path($project, $value);break;case "reference":$value = new Reference($value);break;// any other object params we want to support should go here ...}} // if hint !== null} // if not setrefid} // if is slot-listenertry {$project->log(" -calling setter ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);$method->invoke($element, $value);} catch(Exception $exc) {throw new BuildException($exc);}}/** Adds PCDATA areas.*/function addText(Project $project, $element, $text) {if ($this->methodAddText === null) {$msg = $this->getElementName($project, $element)." doesn't support nested text data.";throw new BuildException($msg);}try {$method = $this->methodAddText;$method->invoke($element, $text);} catch (Exception $exc) {throw new BuildException($exc);}}/*** Creates a named nested element.** Valid creators can be in the form createFoo() or addFoo(Bar).* @return object Returns the nested element.* @throws BuildException*/function createElement(Project $project, $element, $elementName) {$addMethod = "add".strtolower($elementName);$createMethod = "create".strtolower($elementName);$nestedElement = null;if (isset($this->nestedCreators[$createMethod])) {$method = $this->nestedCreators[$createMethod];try { // try to invoke the creator method on object$project->log(" -calling creator ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);$nestedElement = $method->invoke($element);} catch (Exception $exc) {throw new BuildException($exc);}} elseif (isset($this->nestedCreators[$addMethod])) {$method = $this->nestedCreators[$addMethod];// project components must use class hints to support the add methodstry { // try to invoke the adder method on object$project->log(" -calling adder ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);// we've already assured that correct num of params// exist and that method is using class hints$params = $method->getParameters();$classname = null;if (($hint = $params[0]->getClass()) !== null) {$classname = $hint->getName();}// create a new instance of the object and add it via $addMethod$nestedElement = new $classname();$method->invoke($element, $nestedElement);} catch (Exception $exc) {throw new BuildException($exc);}} else {$msg = $this->getElementName($project, $element) . " doesn't support the '$elementName' creator/adder.";throw new BuildException($msg);}if ($nestedElement instanceof ProjectComponent) {$nestedElement->setProject($project);}return $nestedElement;}/*** Creates a named nested element.* @return void* @throws BuildException*/function storeElement($project, $element, $child, $elementName = null) {if ($elementName === null) {return;}$storer = "addconfigured".strtolower($elementName);if (isset($this->nestedStorers[$storer])) {$method = $this->nestedStorers[$storer];try {$project->log(" -calling storer ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);$method->invoke($element, $child);} catch (Exception $exc) {throw new BuildException($exc);}}}/** Does the introspected class support PCDATA? */function supportsCharacters() {return ($this->methodAddText !== null);}/** Return all attribues supported by the introspected class. */function getAttributes() {$attribs = array();foreach (array_keys($this->attributeSetters) as $setter) {$attribs[] =$this->getPropertyName($setter, "set");}return $attribs;}/** Return all nested elements supported by the introspected class. */function getNestedElements() {return $this->nestedTypes;}/*** Get the the name for an element.* When possible the full classnam (phing.tasks.system.PropertyTask) will* be returned. If not available (loaded in taskdefs or typedefs) then the* XML element name will be returned.** @param Project $project* @param object $element The Task or type element.* @return string Fully qualified class name of element when possible.*/function getElementName(Project $project, $element) {$taskdefs = $project->getTaskDefinitions();$typedefs = $project->getDataTypeDefinitions();// check if class of element is registered with project (tasks & types)// most element types don't have a getTag() method$elClass = get_class($element);if (!in_array('getTag', get_class_methods($elClass))) {// loop through taskdefs and typesdefs and see if the class name// matches (case-insensitive) any of the classes in thereforeach(array_merge($taskdefs, $typedefs) as $elName => $class) {if (0 === strcasecmp($elClass, StringHelper::unqualify($class))) {return $class;}}return "$elClass (unknown)";} else {// ->getTag() method does exist, so use it$elName = $element->getTag();if (isset($taskdefs[$elName])) {return $taskdefs[$elName];} elseif (isset($typedefs[$elName])) {return $typedefs[$elName];} else {return "$elName (unknown)";}}}/** extract the name of a property from a method name - subtracting a given prefix. */function getPropertyName($methodName, $prefix) {$start = strlen($prefix);return strtolower(substr($methodName, $start));}/*** Prints warning message to screen if -debug was used.*/function warn($msg) {if (Phing::getMsgOutputLevel() === Project::MSG_DEBUG) {print("[IntrospectionHelper] " . $msg . "\n");}}}