Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Docs/Apiref.php
<?php
/**
 * 
 * Recursively parses a class directory for API reference documentation.
 * 
 * @category Solar
 * 
 * @package Solar_Docs Tools for building API documentation from source code.
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Apiref.php 4534 2010-04-23 16:43:23Z pmjones $
 * 
 * @todo parse constants
 * 
 * @todo report when a method is missing documentation (at least a summary)
 * 
 * @todo report when a property is missing documentation (at least a summary)
 * 
 * @todo actually set up a log object
 * 
 */
class Solar_Docs_Apiref extends Solar_Base
{
    /**
     * 
     * Default configuration values.
     * 
     * @config dependency phpdoc A Solar_Docs_Phpdoc dependency.
     * 
     * @config dependency log A Solar_Log dependency.
     * 
     * @config string unknown When a type is unknown or not specified,
     *   use this value instead.
     * 
     * @var array
     * 
     */
    protected $_Solar_Docs_Apiref = array(
        'phpdoc'  => null,
        'log'     => array(
            'adapter' => 'Solar_Log_Adapter_Echo',
            'format'  => '%m',
        ),
        'unknown' => 'void',
    );
    
    /** 
     * 
     * Solar_Log instance.
     * 
     * @var Solar_Log
     * 
     */
    protected $_log;
    
    /**
     * 
     * Class for parsing PHPDoc comment blocks.
     * 
     * @var Solar_Docs_Phpdoc
     * 
     */
    protected $_phpdoc;
    
    /** 
     * 
     * When generating log notices, ignore these class methods and
     * properties.
     * 
     * @var string
     * 
     * @todo replace with a check for "built-in" classes?
     * 
     */
    protected $_ignore = array(
        'Exception' => array(
            'methods' => array(
                '__clone',
                'getMessage',
                'getCode',
                'getFile',
                'getLine',
                'getPrevious',
                'getTrace',
                'getTraceAsString',
            ),
            'properties' => array(
                'message',
                'code',
                'file',
                'line',
            ),
        ),
    );
    
    /** 
     * 
     * The entire API as a structured array.
     * 
     * {{code: php
     *     $api = array(
     *         classname => array(
     *             summ => string, // phpdoc summary
     *             narr => string, // phpdoc narrative
     *             tech => array(...), // technical phpdoc @tags
     *             from => array(...), // parent classes
     *             constants => array(
     *                 name => array(
     *                     type => string,
     *                     value => string,
     *                 ), // constantname
     *             ), // constants
     *             config_keys => array(
     *                 name => array(
     *                     name => string,
     *                     type => string,
     *                     summ => string,
     *                     value => mixed,
     *                 ),
     *             ), // config_keys
     *             properties => array(
     *                 name => array(
     *                     name => string,
     *                     summ => string,
     *                     narr => string,
     *                     tech => array(...),
     *                     type => string,
     *                     access => string,
     *                     static => bool,
     *                     from => string,
     *                 ), // propertyname
     *             ), // properties
     *             methods => array(
     *                 name => array(
     *                     name => string,
     *                     summ => string,
     *                     narr => string,
     *                     tech => array(...),
     *                     access => string,
     *                     static => bool,
     *                     final => bool,
     *                     return => string,
     *                     from => string,
     *                     params => array(
     *                         name => array(
     *                             name => string,
     *                             type => string,
     *                             summ => string,
     *                             byref => bool,
     *                             optional => bool,
     *                             default => mixed,
     *                         ), // paramname
     *                     ), // params
     *                 ), // methodname
     *             ), // methods
     *         ), // classname
     *     ); // $this->api
     * }}
     * 
     * @var array
     * 
     */
    public $api = array();
    
    /**
     * 
     * An array of all packages discovered.
     * 
     * Key is the package name, value is an array of all classes in that
     * package.
     * 
     * @var array
     * 
     */
    public $packages = array();
    
    /**
     * 
     * An array of all subpackages discovered.
     * 
     * Key is the subpackage name, value is an array of all classes in that
     * subpackage.
     * 
     * @var array
     * 
     */
    public $subpackages = array();
    
    /**
     * 
     * Post-construction tasks to complete object construction.
     * 
     * @return void
     * 
     */
    protected function _postConstruct()
    {
        parent::_postConstruct();
        
        // PHPDoc parser
        $this->_phpdoc = Solar::dependency(
            'Solar_Docs_Phpdoc',
            $this->_config['phpdoc']
        );
        
        // Logger
        $this->_log = Solar::dependency(
            'Solar_Log',
            $this->_config['log']
        );
    }
    
    /**
     * 
     * Adds classes from a file hierarchy.
     * 
     * @param string $base The base of the class hierarchy, typically
     * the base PEAR library path.
     * 
     * @param string $class Start with this class in the hierarchy.
     * 
     * @return void
     * 
     */
    public function addFiles($base, $class = null)
    {
        $map = Solar::factory('Solar_Class_Map');
        $map->setBase($base);
        $source = $map->fetch($class);
        foreach ($source as $class => $file) {
            require_once($file);
            $this->addClass($class, $file);
        }
    }
    
    /**
     * 
     * Adds a class to the API docs.
     * 
     * @param string $class The class to add to the docs.
     * 
     * @param string $file The file name in which the class is defined.
     * 
     * @return bool True if the class was added, false if not.
     * 
     */
    public function addClass($class, $file)
    {
        if (! class_exists($class)) {
            return false;
        }
        
        $reflect = new ReflectionClass($class);
        
        // add top-level class docs
        $this->api[$class] = $this->_phpdoc->parse($reflect->getDocComment());
        
        // definition info
        $this->api[$class]['abstract'] = $reflect->isAbstract() ? 'abstract' : false;
        $this->api[$class]['final'] = $reflect->isFinal() ? 'final' : false;
        $this->api[$class]['interface'] = $reflect->isInterface() ? 'interface' : false;
        
        // needs a summary line
        if (empty($this->api[$class]['summ'])) {
            $this->_log($class, "class '$class' has no summary");
        }
        
        // add to the package list
        if (empty($this->api[$class]['tech']['package'])) {
            $this->_log($class, "class '$class' has no @package tag");
            $this->api[$class]['tech']['package'] = null;
        } else {
            // retain in the class list
            $name = $this->api[$class]['tech']['package']['name'];
            $this->packages[$name]['list'][] = $class;
            // if a summary is present in the class, and no summary
            // has been retained yet, then retain it.
            $summ    = $this->api[$class]['tech']['package']['summ'];
            $already = ! empty($this->packages[$name]['summ']);
            if ($summ && ! $already) {
                $this->packages[$name]['summ'] = $summ;
            }
        }
        
        // optionally add to the subpackage list
        if (! empty($this->api[$class]['tech']['subpackage'])) {
            $name = $this->api[$class]['tech']['subpackage']['name'];
            $this->subpackages[$name][] = $class;
        }
        
        // add the class parents, properties and methods
        $this->_addParents($class);
        $this->_addConstants($class, $file);
        $this->_addConfigKeys($class);
        $this->_addProperties($class);
        $this->_addMethods($class);
        
        // done!
        return true;
    }
        
    
    /**
     * 
     * Adds the inheritance hierarchy for a given class.
     * 
     * @param string $class The class name.
     * 
     * @return void
     * 
     */
    protected function _addParents($class)
    {
        $parent = $class;
        $parents = array();
        while ($parent = get_parent_class($parent)) {
            $parents[] = $parent;
        }
        $this->api[$class]['from'] = array_reverse($parents);
    }
    
    /**
     * 
     * Adds the constant reflections for a given class.
     * 
     * The Reflection API does not support doc comments for constants yet,
     * which means we have to do a lot of extra work here to extract the
     * comments and their related information.
     * 
     * @param string $class The class name.
     * 
     * @param string $file The file in which the class is defined.
     * 
     * @return void
     * 
     */
    protected function _addConstants($class, $file)
    {
        // get constants; bail out early if there are none
        $reflect = new ReflectionClass($class);
        $list = $reflect->getConstants();
        if (! $list) {
            $this->api[$class]['constants'] = array();
            return;
        }
        
        // retain basic constants information
        $const = array();
        foreach ($list as $key => $val) {
            $const[$key] = array(
                'name' => $key,
                'summ' => '',
                'narr' => '',
                'tech' => '',
                'type' => gettype($val),
                'value' => var_export($val, true),
                // @todo add 'from' with inheritance check
            );
        }
        
        // re-purpose $list to be a regular expression clause
        $list = implode('|', array_keys($list));
        
        // the contents of the class file
        $text     = file_get_contents($file);
        
        // the length of the file
        $len      = strlen($text);
        
        // the current docblock text
        $block    = null;
        
        // are we in a docblock?
        $in_block = false;
        
        // manually retrieve docblocks from the file
        for ($pos = 0; $pos < $len; $pos ++) {
    
            // retain the current character
            $char = $text[$pos];
    
            // are we at a "/**" opener?
            if (substr($text, $pos, 3) == '/**') {
                $in_block = true;
                $block = null;
            }
    
            // are we in a docblock?
            if ($in_block) {
                $block .= $char;
            }
    
            // are we leaving a docblock?
            if (substr($text, $pos - 1, 2) == '*/') {
                
                // yes, no longer in a docblock
                $in_block = false;
        
                // is the docblock followed by a constant declaration?
                $expr = '/^[\s\n]*const[\s\n]+(' . $list . ')[\s\n]*=/';
                $next = substr($text, $pos + 1);
                preg_match($expr, $next, $matches);
                
                // yes, parse and retain the docblock info
                if (! empty($matches[1])) {
                    $name = $matches[1];
                    $docs = $this->_phpdoc->parse($block);
                    $const[$name]['summ'] = $docs['summ'];
                    $const[$name]['narr'] = $docs['narr'];
                    $const[$name]['tech'] = $docs['tech'];
                }
            }
        }
        // retain the constants array
        $this->api[$class]['constants'] = $const;
    }
    
    /**
     * 
     * Adds the property reflections for a given class.
     * 
     * @param string $class The class name.
     * 
     * @return void
     * 
     */
    protected function _addProperties($class)
    {
        $this->api[$class]['properties'] = array();
        $reflect = new ReflectionClass($class);
        
        foreach ($reflect->getProperties() as $prop) {
        
            // the property name
            $name = $prop->getName();
            
            // comment docs
            $docs = $this->_phpdoc->parse($prop->getDocComment());
            
            // basic properties
            $info = array(
                'name'   => $name,
                'summ'   => $docs['summ'],
                'narr'   => $docs['narr'],
                'tech'   => $docs['tech'],
                'type'   => null,
                'access' => null,
                'static' => $prop->isStatic() ? 'static' : false,
                'from' => false,
            );
            
            // set the access type
            if ($prop->isPublic()) {
                $info['access'] = "public";
            } elseif ($prop->isProtected()) {
                $info['access'] = "protected";
            } elseif ($prop->isPrivate()) {
                $info['access'] = "private";
            }
            
            // is this a class we ignore?
            // use the declaring class, not the current class, because
            // the property may be inherited.
            $decl = $prop->getDeclaringClass()->getName();
            $ignore = (array) @$this->_ignore[$decl]['properties'];
            
            // is there a summary line?
            if (empty($docs['summ'])) {
                // no summary line.  
                if (! in_array($name, $ignore)) {
                    // not in the list of ignored properties
                    $this->_log($class, "property '$name' has no summary");
                }
            }
            
            // does @var exist?
            if (empty($docs['tech']['var']['type'])) {
                // no @var type.  
                if (! in_array($name, $ignore)) {
                    // not in the list of ignored properties
                    $this->_log($class, "property '$name' has no @var type");
                }
            } else {
                $info['type'] = $docs['tech']['var']['type'];
            }
            
            // save in the API
            $this->api[$class]['properties'][$name] = $info;
            
            // was it inherited after all?
            $inherited = $this->_isInheritedProperty($class, $prop);
            $this->api[$class]['properties'][$name]['from'] = $inherited;
            
        }
        
        // sort them
        ksort($this->api[$class]['properties']);
    }
    
    /**
     * 
     * Adds the Solar configuration keys for a given class.
     * 
     * @param string $class The class name.
     * 
     * @return void
     * 
     */
    protected function _addConfigKeys($class)
    {
        $this->api[$class]['config_keys'] = array();
        
        // holding place for config key names and values
        $name_value = array();
        
        // holding place for tech info about @key phpdoc tags
        $tech = array();
        
        // get the parent classes and add the class itself
        $list = $this->api[$class]['from'];
        array_push($list, $class);
        foreach ($list as $item) {
            $reflect = new ReflectionClass($item);
            // all properties
            $vars = $reflect->getDefaultProperties();
            // the name of the config property
            $cvar = "_$item";
            // is there a config property?
            if (! empty($vars[$cvar])) {
                
                // merge name-value pairs with pre-existing
                $name_value = array_merge($name_value, $vars[$cvar]);
                
                // parse the docblock on the config var
                $prop = $reflect->getProperty($cvar);
                $docs = $this->_phpdoc->parse($prop->getDocComment());
                if (! empty($docs['tech']['key'])) {
                    foreach ($docs['tech']['key'] as $name => $info) {
                        $tech[$name] = $info;
                    }
                }
            }
        }
        
        foreach ($name_value as $name => $value) {
            if ($value === null) {
                $value = 'null'; // so that we get lower-case
            } else {
                $value = var_export($value, true);
            }
            
            if (empty($tech[$name]['type'])) {
                $tech[$name]['type'] = "unknown";
                $this->_log($class, "config key '$name' has no type");
            }
            
            if (empty($tech[$name]['summ'])) {
                $tech[$name]['summ'] = "No summary.";
                $this->_log($class, "config key '$name' has no summary");
            }
            
            $this->api[$class]['config_keys'][$name] = array(
                'name'  => $name,
                'type'  => $tech[$name]['type'],
                'summ'  => $tech[$name]['summ'],
                'value' => $value,
            );
        }
    }
    
    /**
     * 
     * Adds the method reflections for a given class.
     * 
     * @param string $class The class name.
     * 
     * @return void
     * 
     */
    protected function _addMethods($class)
    {
        $this->api[$class]['methods'] = array();
        
        $reflect = new ReflectionClass($class);
        
        foreach ($reflect->getMethods() as $method) {
            
            // get the method name
            $name = $method->getName();
            
            // parse the doc comments
            $docs = $this->_phpdoc->parse($method->getDocComment());
            
            // the basic method information
            $info = array(
                'from'     => false,
                'name'     => $name,
                'summ'     => $docs['summ'],
                'narr'     => $docs['narr'],
                'tech'     => $docs['tech'],
                'abstract' => $method->isAbstract() ? 'abstract' : false,
                'access'   => null,
                'static'   => $method->isStatic() ? 'static' : false,
                'final'    => $method->isFinal() ? 'final' : false,
                'return'   => null,
                'byref'    => $method->returnsReference() ? '&' : false,
                'params'   => array(),
            );
            
            // add the access visibility
            if ($method->isPublic()) {
                $info['access'] = 'public';
            } elseif ($method->isProtected()) {
                $info['access'] = 'protected';
            } elseif ($method->isPrivate()) {
                $info['access'] = 'private';
            }
            
            // is this a class we ignore?
            // use the declaring class, not the current class, because
            // the property may be inherited.
            $decl = $method->getDeclaringClass()->getName();
            $ignore = (array) @$this->_ignore[$decl]['methods'];
            
            // is there a summary line?
            if (empty($docs['summ'])) {
                // no summary line.  
                if (! in_array($name, $ignore)) {
                    // not in the list of ignored methods
                    $this->_log($class, "method '$name' has no summary");
                }
            }
            
            // find the return type in the technical docs
            if ($method->isConstructor()) {
                // it's a constructor, so it returns its own class
                $info['return'] = $class;
            } elseif (! empty($docs['tech']['return']['type'])) {
                // return type comes from tech docs
                $info['return'] = $docs['tech']['return']['type'];
            } else {
                // no return type noted in the class docs
                $info['return'] = $this->_config['unknown'];
                
                // can we ignore this lack of type?
                if (! in_array($name, $ignore)) {
                    // not to be ignored
                    $unknown = $this->_config['unknown'];
                    $this->_log($class, "method '$name' has unknown @return type, used '$unknown'");
                }
            }
            
            // add the parameters
            $info['params'] = $this->_getParameters($class, $method, $docs['tech']);
            
            // save in the API
            $this->api[$class]['methods'][$name] = $info;
            
            // was it inherited after all?
            $inherited = $this->_isInheritedMethod($class, $method);
            $this->api[$class]['methods'][$name]['from'] = $inherited;
        }
        
        // sort them
        ksort($this->api[$class]['methods']);
    }
    
    /**
     * 
     * Reports the class, if any, a method is inherited from and identical to.
     * 
     * @param string $class The class to check.
     * 
     * @param ReflectionMethod $method The method to check.
     * 
     * @return string The class from which the method was inherited, but only
     * if the modifiers, parameters, and comments are identical.
     * 
     */
    protected function _isInheritedMethod($class, ReflectionMethod $method)
    {
        // if declared in the same class, then it's not inherited.
        $decl = $method->getDeclaringClass()->getName();
        if ($class != $decl) {
            return $decl;
        } else {
            return false;
        }
    }
    
    /**
     * 
     * Reports the class, if any, a property is inherited from and identical to.
     * 
     * @param string $class The class to check.
     * 
     * @param ReflectionProperty $property The property to check.
     * 
     * @return string The class from which the property was inherited, but only
     * if the modifiers and comments are identical.
     * 
     */
    protected function _isInheritedProperty($class, ReflectionProperty $property)
    {
        // if declared in the same class, then it's not inherited.
        $decl = $property->getDeclaringClass()->getName();
        if ($class != $decl) {
            return $decl;
        } else {
            return false;
        }
    }
    
    /**
     * 
     * Returns the parameters for a ReflectionMethod.
     * 
     * @param string $class The class name.
     * 
     * @param ReflectionMethod $method A ReflectionMethod object to get parameters for.
     * 
     * @param array $tech A technical information array derived from Solar_Docs_Phpdoc.
     * 
     * @return array An array of parameter specifications.
     * 
     */
    protected function _getParameters($class, ReflectionMethod $method, $tech)
    {
        $params = array();
        $methodname = $method->getName();
        
        // find each of the parameters
        foreach ($method->getParameters() as $param) {
            $name = $param->getName();
            $params[$name] = array(
                'name'     => $name,
                'type'     => 'unknown',
                'summ'     => null,
                'byref'    => $param->isPassedByReference() ? '&' : false,
                'optional' => $param->isOptional(),
                'default'  => $param->isOptional() ? $param->getDefaultValue() : null,
            );
            
            // add the type
            if ($param->getClass()) {
                
                // the type comes from a typehint.
                $params[$name]['type'] = $param->getClass();
                
                // hack, because of return differences between PHP5.1.4
                // and earlier PHP5.1.x versions.  otherwise you get
                // things like "Object id #31" as the type.
                if (is_object($params[$name]['type'])) {
                    $params[$name]['type'] = $params[$name]['type']->name;
                }
                
            } elseif (! empty($tech['param'][$name]['type'])) {
                // the type comes from the tech docs
                $params[$name]['type'] = $tech['param'][$name]['type'];
            } else {
                // no typehint, and not in the class docs
                $this->_log($class, "method '$methodname' param '$name' has no type");
            }
            
            // add the summary
            if (! empty($tech['param'][$name]['summ'])) {
                // summary comes from the tech docs
                $params[$name]['summ'] = $tech['param'][$name]['summ'];
            } else {
                // no summary
                $this->_log($class, "method '$methodname' param '$name' has no summary");
            }
        }
        return $params;
    }
    
    /**
     * 
     * Saves a message to the log.
     * 
     * @param string $class The class that the message refers to.
     * 
     * @param string $message The event message.
     * 
     * @return void
     * 
     */
    protected function _log($class, $message)
    {
        $message = "$class: $message";
        $this->_log->save(get_class($this), 'docs', $message);
    }
}
Return current item: SolarPHP