* @link http://www.yiiframework.com/ * @copyright Copyright © 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ * @version $Id$ */ /** * ApiModel represents the documentation for the Yii framework. * @author Qiang Xue * @version $Id$ * @package system.build * @since 1.0 */ class ApiModel { public $classes=array(); public $packages; private $_currentClass; public function build($sourceFiles) { $this->findClasses($sourceFiles); $this->processClasses(); } protected function findClasses($sourceFiles) { $this->classes=array(); foreach($sourceFiles as $file) require_once($file); $classes=array_merge(get_declared_classes(),get_declared_interfaces()); foreach($classes as $class) { $r=new ReflectionClass($class); if(in_array($r->getFileName(),$sourceFiles)) $this->classes[$class]=true; } ksort($this->classes); } protected function processClasses() { $this->packages=array(); foreach($this->classes as $class=>$value) { $doc=$this->processClass(new ReflectionClass($class)); $this->classes[$class]=$doc; $this->packages[$doc->package][]=$class; } ksort($this->packages); // find out child classes for each class or interface foreach($this->classes as $class) { if(isset($class->parentClasses[0])) { $parent=$class->parentClasses[0]; if(isset($this->classes[$parent])) $this->classes[$parent]->subclasses[]=$class->name; } foreach($class->interfaces as $interface) { if(isset($this->classes[$interface])) $this->classes[$interface]->subclasses[]=$class->name; } } } protected function processClass($class) { $doc=new ClassDoc; $doc->name=$class->getName(); $this->_currentClass=$doc->name; for($parent=$class;$parent=$parent->getParentClass();) $doc->parentClasses[]=$parent->getName(); foreach($class->getInterfaces() as $interface) $doc->interfaces[]=$interface->getName(); $doc->isInterface=$class->isInterface(); $doc->isAbstract=$class->isAbstract(); $doc->isFinal=$class->isFinal(); $doc->methods=$this->processMethods($class); $doc->properties=$this->processProperties($class); $doc->signature=($doc->isInterface?'interface ':'class ').$doc->name; if($doc->isFinal) $doc->signature='final '.$doc->signature; if($doc->isAbstract && !$doc->isInterface) $doc->signature='abstract '.$doc->signature; if(in_array('CComponent',$doc->parentClasses)) { $doc->properties=array_merge($doc->properties,$this->processComponentProperties($class)); $doc->events=$this->processComponentEvents($class); } ksort($doc->properties); foreach($doc->properties as $property) { if($property->isProtected) $doc->protectedPropertyCount++; else $doc->publicPropertyCount++; if(!$property->isInherited) $doc->nativePropertyCount++; } foreach($doc->methods as $method) { if($method->isProtected) $doc->protectedMethodCount++; else $doc->publicMethodCount++; if(!$method->isInherited) $doc->nativeMethodCount++; } foreach($doc->events as $event) { if(!$event->isInherited) $doc->nativeEventCount++; } $this->processComment($doc,$class->getDocComment()); return $doc; } protected function processComment($doc,$comment) { $comment=strtr(trim(preg_replace('/^\s*\**( |\t)?/m','',trim($comment,'/'))),"\r",''); if(preg_match('/^\s*@\w+/m',$comment,$matches,PREG_OFFSET_CAPTURE)) { $meta=substr($comment,$matches[0][1]); $comment=trim(substr($comment,0,$matches[0][1])); } else $meta=''; if(($pos=strpos($comment,"\n"))!==false) $doc->introduction=$this->processDescription(substr($comment,0,$pos)); else $doc->introduction=$this->processDescription($comment); $doc->description=$this->processDescription($comment); $this->processTags($doc,$meta); } protected function processDescription($text) { if(($text=trim($text))==='') return ''; $text=preg_replace_callback('/\{@include\s+([^\s\}]+)\s*\}/s',array($this,'processInclude'),$text); $text=preg_replace('/^(\r| |\t)*$/m',"

",$text); $text=preg_replace_callback('/
(.*?)<\/pre>/is',array($this,'processCode'),$text);
		$text=preg_replace_callback('/\{@link\s+([^\s\}]+)(.*?)\}/s',array($this,'processLink'),$text);
		return $text;
	}

	protected function processCode($matches)
	{
		return preg_replace('//','',$matches[0]);
	}

	protected function resolveInternalUrl($url)
	{
		$url=rtrim($url,'()');
		if(($pos=strpos($url,'::'))!==false)
		{
			$class=substr($url,0,$pos);
			$method=substr($url,$pos+2);
		}
		else if(isset($this->classes[$url]))
			return $url;
		else
		{
			$class=$this->_currentClass;
			$method=$url;
		}
		return $this->getMethodUrl($class,$method);
	}

	protected function getMethodUrl($class,$method)
	{
		if(!isset($this->classes[$class]))
			return '';
		if(method_exists($class,$method) || property_exists($class,$method))
			return $class.'::'.$method;
		if(method_exists($class,'get'.$method) || method_exists($class,'set'.$method))
			return $class.'::'.$method;
		if(($parent=get_parent_class($class))!==false)
			return $this->getMethodUrl($parent,$method);
		else
			return '';
	}

	protected function processLink($matches)
	{
		$url=$matches[1];
		if(($text=trim($matches[2]))==='')
			$text=$url;

		if(preg_match('/^(http|ftp):\/\//i',$url))  // an external URL
			return "$text";
		$url=$this->resolveInternalUrl($url);
		return $url===''?$text:'{{'.$url.'|'.$text.'}}';
	}

	protected function processInclude($matches)
	{
		$class=new ReflectionClass($this->_currentClass);
		$fileName=dirname($class->getFileName()).DIRECTORY_SEPARATOR.$matches[1];
		if(is_file($fileName))
			return file_get_contents($fileName);
		else
			return $matches[0];
	}

	protected function processTags($object,$comment)
	{
		$tags=preg_split('/^\s*@/m',$comment,-1,PREG_SPLIT_NO_EMPTY);
		foreach($tags as $tag)
		{
			$segs=preg_split('/\s+/',trim($tag),2);
			$tagName=$segs[0];
			$param=isset($segs[1])?trim($segs[1]):'';
			$tagMethod='tag'.ucfirst($tagName);
			if(method_exists($this,$tagMethod))
				$this->$tagMethod($object,$param);
			else if(property_exists($object,$tagName))
				$object->$tagName=$param;
		}
	}

	protected function processMethods($class)
	{
		$methods=array();
		foreach($class->getMethods() as $method)
		{
			if($method->isPublic() || $method->isProtected())
			{
				$doc=$this->processMethod($class,$method);
				$methods[$doc->name]=$doc;
			}
		}
		ksort($methods);
		return $methods;
	}

	protected function processMethod($class,$method)
	{
		$doc=new MethodDoc;
		$doc->name=$method->getName();
		$doc->definedBy=$method->getDeclaringClass()->getName();
		$doc->isAbstract=$method->isAbstract();
		$doc->isFinal=$method->isFinal();
		$doc->isProtected=$method->isProtected();
		$doc->isStatic=$method->isStatic();
		$doc->isInherited=$doc->definedBy!==$class->getName();

		$doc->input=array();
		foreach($method->getParameters() as $param)
		{
			$p=new ParamDoc;
			$p->name=$param->getName();
			$p->isOptional=$param->isOptional();
			if($param->isDefaultValueAvailable())
				$p->defaultValue=$param->getDefaultValue();
			$doc->input[]=$p;
		}
		reset($doc->input);

		$this->processComment($doc,$method->getDocComment());

		$params=array();
		foreach($doc->input as $param)
		{
			$type=empty($param->type)?'':$this->getTypeUrl($param->type).' ';
			if($param->isOptional)
				$params[]=$type.'$'.$param->name.'='.var_export($param->defaultValue,true);
			else
				$params[]=$type.'$'.$param->name;
		}
		$doc->signature='{{'.$class->name.'::'.$doc->name.'|'.$doc->name.'}}('.implode(', ',$params).')';
		if($doc->output!==null)
			$doc->signature=$this->getTypeUrl($doc->output->type).' '.$doc->signature;
		else
			$doc->signature='void '.$doc->signature;
		if(($modifier=implode(' ',Reflection::getModifierNames($method->getModifiers())))!=='')
			$doc->signature=$modifier.' '.$doc->signature;

		return $doc;
	}

	protected function getTypeUrl($type)
	{
		if(isset($this->classes[$type]) && $type!==$this->_currentClass)
			return '{{'.$type.'|'.$type.'}}';
		else
			return $type;
	}

	protected function processProperties($class)
	{
		$properties=array();
		foreach($class->getProperties() as $property)
		{
			if($property->isPublic() || $property->isProtected())
			{
				$p=$this->processProperty($class,$property);
				$properties[$p->name]=$p;
			}
		}
		return $properties;
	}

	protected function processProperty($class,$property)
	{
		$doc=new PropertyDoc;
		$doc->name=$property->getName();
		$doc->definedBy=$property->getDeclaringClass()->getName();
		$doc->readOnly=false;
		$doc->isStatic=$property->isStatic();
		$doc->isProtected=$property->isProtected();
		$doc->isInherited=$doc->definedBy!==$class->getName();

		$this->processComment($doc,$property->getDocComment());

		$doc->signature='$'.$doc->name.';';
		if($doc->type!==null)
			$doc->signature=$this->getTypeUrl($doc->type) . ' ' . $doc->signature;
		if(($modifier=implode(' ',Reflection::getModifierNames($property->getModifiers())))!=='')
			$doc->signature=$modifier.' '.$doc->signature;

		return $doc;
	}

	protected function processComponentProperties($class)
	{
		$properties=array();
		foreach($class->getMethods() as $method)
		{
			if($this->isPropertyMethod($method) && ($method->isPublic() || $method->isProtected()))
			{
				$p=$this->processComponentProperty($class,$method);
				$properties[$p->name]=$p;
			}
		}
		return $properties;
	}

	protected function processComponentProperty($class,$method)
	{
		$doc=new PropertyDoc;
		$name=$method->getName();
		$doc->name=strtolower($name[3]).substr($name,4);
		$doc->isProtected=$method->isProtected();
		$doc->isStatic=false;
		$doc->readOnly=!$class->hasMethod('set'.substr($name,3));
		$doc->definedBy=$method->getDeclaringClass()->getName();
		$doc->isInherited=$doc->definedBy!==$class->getName();
		$doc->getter=$this->processMethod($class,$method);
		if(!$doc->readOnly)
			$doc->setter=$this->processMethod($class,$class->getMethod('set'.substr($name,3)));

		$this->processComment($doc,$method->getDocComment());

		return $doc;
	}

	protected function processComponentEvents($class)
	{
		$events=array();
		foreach($class->getMethods() as $method)
		{
			if($this->isEventMethod($method) && ($method->isPublic() || $method->isProtected()))
			{
				$e=$this->processComponentEvent($class,$method);
				$events[$e->name]=$e;
			}
		}
		return $events;
	}

	protected function processComponentEvent($class,$method)
	{
		$doc=new EventDoc;
		$doc->name=$method->getName();
		$doc->definedBy=$method->getDeclaringClass()->getName();
		$doc->isInherited=$doc->definedBy!==$class->getName();
		$doc->trigger=$this->processMethod($class,$method);

		$this->processComment($doc,$method->getDocComment());

		return $doc;
	}

	protected function tagParam($object,$comment)
	{
		if($object instanceof FunctionDoc)
		{
			$param=current($object->input);
			if($param!==false)
			{
				$segs=preg_split('/\s+/',$comment,2);
				$param->type=$segs[0];
				if(preg_match('/\[\s*\]/',$param->type))
					$param->type='array';
				if(isset($segs[1]))
					$param->description=$this->processDescription($segs[1]);
				next($object->input);
			}
		}
	}

	protected function tagReturn($object,$comment)
	{
		$segs=preg_split('/\s+/',$comment,2);
		if($object instanceof FunctionDoc)
		{
			$object->output=new ParamDoc;
			$object->output->type=$segs[0];
			if(isset($segs[1]))
				$object->output->description=$this->processDescription($segs[1]);
		}
		else if($object instanceof PropertyDoc)
		{
			$object->type=$segs[0];
			if(isset($segs[1]) && empty($object->description))
			{
				if(($pos=strpos($segs[1],'.'))!==false)
					$object->introduction=$this->processDescription(substr($segs[1],0,$pos+1));
				else
					$object->introduction=$this->processDescription($segs[1]);
				$object->description=$this->processDescription($segs[1]);
			}
		}
	}

	protected function tagVar($object,$comment)
	{
		if($object instanceof PropertyDoc)
		{
			$segs=preg_split('/\s+/',$comment,2);
			$object->type=$segs[0];
			if(isset($segs[1]) && empty($object->description))
			{
				if(($pos=strpos($segs[1],'.'))!==false)
					$object->introduction=$this->processDescription(substr($segs[1],0,$pos+1));
				else
					$object->introduction=$this->processDescription($segs[1]);
				$object->description=$this->processDescription($segs[1]);
			}
		}
	}

	protected function tagSee($object,$comment)
	{
		$segs=preg_split('/\s+/',trim($comment),2);
		$matches[1]=$segs[0];
		$matches[2]=isset($segs[1])?$segs[1]:'';
		$object->see[]=$this->processLink($matches);
	}

	protected function isPropertyMethod($method)
	{
		$methodName=$method->getName();
		return $method->getNumberOfRequiredParameters()===0
				&& !$method->isStatic()
				&& strncasecmp($methodName,'get',3)===0
				&& isset($methodName[3]);
	}

	protected function isEventMethod($method)
	{
		$methodName=$method->getName();
		return strncasecmp($methodName,'on',2)===0
				&& !$method->isStatic()
				&& isset($methodName[2]);
	}

	protected function getClassFiles($basePath)
	{
		$files=array();
		$folder=opendir($basePath);
		while($file=readdir($folder))
		{
			if($file==='.' || $file==='..')
				continue;
			$fullPath=realpath($basePath.DIRECTORY_SEPARATOR.$file);
			if($this->isValidPath($fullPath))
			{
				if(is_file($fullPath))
					$files[]=$fullPath;
				else
					$files=array_merge($files,$this->getClassFiles($fullPath));
			}
		}
		closedir($folder);
		return $files;
	}

	protected function isValidPath($path)
	{
		if(is_file($path) && substr($path,-4)!=='.php')
			return false;
		$path=strtr($path,'\\','/');
		foreach($this->_excludes as $exclude)
		{
			if(($exclude[0]==='/' && $this->_sourcePath.$exclude===$path) || ($exclude[0]!=='/' && basename($path)===$exclude))
				return false;
		}
		return true;
	}

	protected function findTargets()
	{
		$oldClasses=get_declared_classes();
		$oldInterfaces=get_declared_interfaces();
		$oldFunctions=get_defined_functions();
		$oldConstants=get_defined_constants(true);

		$classFiles=$this->getClassFiles($this->_sourcePath);
		require_once($this->_sourcePath.'/yii.php');
		foreach($classFiles as $classFile)
			require_once($classFile);

		$classes=array_values(array_diff(get_declared_classes(),$oldClasses));
		$interfaces=array_values(array_diff(get_declared_interfaces(),$oldInterfaces));
		$classes=array_merge($classes,$interfaces);

		$n=count($classes);
		for($i=0;$i<$n;++$i)
		{
			$class=new ReflectionClass($classes[$i]);
			$fileName=strtr($class->getFileName(),'\\','/');
			foreach($this->_excludes as $exclude)
			{
				if(($exclude[0]==='/' && strpos($fileName,$this->_sourcePath.$exclude)===0))
				{
					unset($classes[$i]);
					break;
				}
			}
		}

		sort($classes);
		$newFunctions=get_defined_functions();
		$newConstants=get_defined_constants(true);
		$functions=array_values(array_diff($newFunctions['user'],$oldFunctions['user']));
		$constants=$newConstants['user'];

		return array($classes,$functions,$constants);
	}
}

class BaseDoc
{
	public $name;
	public $since;
	public $see;
	public $introduction;
	public $description;
}

class ClassDoc extends BaseDoc
{
	public $parentClasses=array();
	public $subclasses=array();
	public $interfaces=array();
	public $isInterface;
	public $isAbstract;
	public $isFinal;

	public $signature;

	public $properties=array();
	public $methods=array();
	public $events=array();
	public $constants=array();

	public $protectedPropertyCount=0;
	public $publicPropertyCount=0;
	public $protectedMethodCount=0;
	public $publicMethodCount=0;

	public $nativePropertyCount=0;
	public $nativeMethodCount=0;
	public $nativeEventCount=0;

	public $package;
	public $version;
}

class PropertyDoc extends BaseDoc
{
	public $isProtected;
	public $isStatic;
	public $readOnly;
	public $isInherited;
	public $definedBy;

	public $type;
	public $signature;

	public $getter;
	public $setter;
}

class FunctionDoc extends BaseDoc
{
	public $signature;
	public $input=array();
	public $output;
}

class MethodDoc extends FunctionDoc
{
	public $isAbstract;
	public $isFinal;
	public $isProtected;
	public $isStatic;
	public $isInherited;
	public $definedBy;
}

class EventDoc extends BaseDoc
{
	public $isInherited;
	public $definedBy;
	public $trigger;
}

class ParamDoc
{
	public $name;
	public $description;
	public $type;
	public $isOptional;
	public $defaultValue;
}