| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 | <?php/** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */namespace yii\apidoc\models;use phpDocumentor\Reflection\FileReflector;use yii\base\Component;/** * * @author Carsten Brandt <mail@cebe.cc> * @since 2.0 */class Context extends Component{    /**     * @var array list of php files that have been added to this context.     */    public $files = [];    /**     * @var ClassDoc[]     */    public $classes = [];    /**     * @var InterfaceDoc[]     */    public $interfaces = [];    /**     * @var TraitDoc[]     */    public $traits = [];    /**     * @var array     */    public $errors = [];    /**     * @var array     * @since 2.0.6     */    public $warnings = [];    /**     * Returning TypeDoc for a type given     * @param string $type     * @return null|ClassDoc|InterfaceDoc|TraitDoc     */    public function getType($type)    {        $type = ltrim($type, '\\');        if (isset($this->classes[$type])) {            return $this->classes[$type];        } elseif (isset($this->interfaces[$type])) {            return $this->interfaces[$type];        } elseif (isset($this->traits[$type])) {            return $this->traits[$type];        }        return null;    }    /**     * Adds file to context     * @param string $fileName     */    public function addFile($fileName)    {        $this->files[$fileName] = sha1_file($fileName);        $reflection = new FileReflector($fileName, true);        $reflection->process();        foreach ($reflection->getClasses() as $class) {            $class = new ClassDoc($class, $this, ['sourceFile' => $fileName]);            $this->classes[$class->name] = $class;        }        foreach ($reflection->getInterfaces() as $interface) {            $interface = new InterfaceDoc($interface, $this, ['sourceFile' => $fileName]);            $this->interfaces[$interface->name] = $interface;        }        foreach ($reflection->getTraits() as $trait) {            $trait = new TraitDoc($trait, $this, ['sourceFile' => $fileName]);            $this->traits[$trait->name] = $trait;        }    }    /**     * Updates references     */    public function updateReferences()    {        // update all subclass references        foreach ($this->classes as $class) {            $className = $class->name;            while (isset($this->classes[$class->parentClass])) {                $class = $this->classes[$class->parentClass];                $class->subclasses[] = $className;            }        }        // update interfaces of subclasses        foreach ($this->classes as $class) {            $this->updateSubclassInterfacesTraits($class);        }        // update implementedBy and usedBy for interfaces and traits        foreach ($this->classes as $class) {            foreach ($class->traits as $trait) {                if (isset($this->traits[$trait])) {                    $trait = $this->traits[$trait];                    $trait->usedBy[] = $class->name;                    $class->properties = array_merge($trait->properties, $class->properties);                    $class->methods = array_merge($trait->methods, $class->methods);                }            }            foreach ($class->interfaces as $interface) {                if (isset($this->interfaces[$interface])) {                    $this->interfaces[$interface]->implementedBy[] = $class->name;                    if ($class->isAbstract) {                        // add not implemented interface methods                        foreach ($this->interfaces[$interface]->methods as $method) {                            if (!isset($class->methods[$method->name])) {                                $class->methods[$method->name] = $method;                            }                        }                    }                }            }        }        foreach ($this->interfaces as $interface) {            foreach ($interface->parentInterfaces as $pInterface) {                if (isset($this->interfaces[$pInterface])) {                    $this->interfaces[$pInterface]->implementedBy[] = $interface->name;                }            }        }        // inherit docs        foreach ($this->classes as $class) {            $this->inheritDocs($class);        }        // inherit properties, methods, contants and events to subclasses        foreach ($this->classes as $class) {            $this->updateSubclassInheritance($class);        }        foreach ($this->interfaces as $interface) {            $this->updateSubInterfaceInheritance($interface);        }        // add properties from getters and setters        foreach ($this->classes as $class) {            $this->handlePropertyFeature($class);        }        // TODO reference exceptions to methods where they are thrown    }    /**     * Add implemented interfaces and used traits to subclasses     * @param ClassDoc $class     */    protected function updateSubclassInterfacesTraits($class)    {        foreach ($class->subclasses as $subclass) {            $subclass = $this->classes[$subclass];            $subclass->interfaces = array_unique(array_merge($subclass->interfaces, $class->interfaces));            $subclass->traits = array_unique(array_merge($subclass->traits, $class->traits));            $this->updateSubclassInterfacesTraits($subclass);        }    }    /**     * Add implemented interfaces and used traits to subclasses     * @param ClassDoc $class     */    protected function updateSubclassInheritance($class)    {        foreach ($class->subclasses as $subclass) {            $subclass = $this->classes[$subclass];            $subclass->events = array_merge($class->events, $subclass->events);            $subclass->constants = array_merge($class->constants, $subclass->constants);            $subclass->properties = array_merge($class->properties, $subclass->properties);            $subclass->methods = array_merge($class->methods, $subclass->methods);            $this->updateSubclassInheritance($subclass);        }    }    /**     * Add methods to subinterfaces     * @param InterfaceDoc $class     */    protected function updateSubInterfaceInheritance($interface)    {        foreach ($interface->implementedBy as $subInterface) {            if (isset($this->interfaces[$subInterface])) {                $subInterface = $this->interfaces[$subInterface];                $subInterface->methods = array_merge($interface->methods, $subInterface->methods);                $this->updateSubInterfaceInheritance($subInterface);            }        }    }    /**     * Inhertit docsblocks using `@inheritDoc` tag.     * @param ClassDoc $class     * @see http://phpdoc.org/docs/latest/guides/inheritance.html     */    protected function inheritDocs($class)    {        // inherit for properties        foreach ($class->properties as $p) {            if ($p->hasTag('inheritdoc') && ($inheritTag = $p->getFirstTag('inheritdoc')) !== null) {                $inheritedProperty = $this->inheritPropertyRecursive($p, $class);                if (!$inheritedProperty) {                    $this->errors[] = [                        'line' => $p->startLine,                        'file' => $class->sourceFile,                        'message' => "Method {$p->name} has no parent to inherit from in {$class->name}.",                    ];                    continue;                }                // set all properties that are empty.                foreach (['shortDescription', 'type', 'types'] as $property) {                    if (empty($p->$property) || is_string($p->$property) && trim($p->$property) === '') {                        $p->$property = $inheritedProperty->$property;                    }                }                // descriptions will be concatenated.                $p->description = trim($p->description) . "\n\n"                    . trim($inheritedProperty->description) . "\n\n"                    . $inheritTag->getContent();                $p->removeTag('inheritdoc');            }        }        // inherit for methods        foreach ($class->methods as $m) {            if ($m->hasTag('inheritdoc') && ($inheritTag = $m->getFirstTag('inheritdoc')) !== null) {                $inheritedMethod = $this->inheritMethodRecursive($m, $class);                if (!$inheritedMethod) {                    $this->errors[] = [                        'line' => $m->startLine,                        'file' => $class->sourceFile,                        'message' => "Method {$m->name} has no parent to inherit from in {$class->name}.",                    ];                    continue;                }                // set all properties that are empty.                foreach (['shortDescription', 'return', 'returnType', 'returnTypes', 'exceptions'] as $property) {                    if (empty($m->$property) || is_string($m->$property) && trim($m->$property) === '') {                        $m->$property = $inheritedMethod->$property;                    }                }                // descriptions will be concatenated.                $m->description = trim($m->description) . "\n\n"                    . trim($inheritedMethod->description) . "\n\n"                    . $inheritTag->getContent();                foreach ($m->params as $i => $param) {                    if (!isset($inheritedMethod->params[$i])) {                        $this->errors[] = [                            'line' => $m->startLine,                            'file' => $class->sourceFile,                            'message' => "Method param $i does not exist in parent method, @inheritdoc not possible in {$m->name} in {$class->name}.",                        ];                        continue;                    }                    if (empty($param->description) || trim($param->description) === '') {                        $param->description = $inheritedMethod->params[$i]->description;                    }                    if (empty($param->type) || trim($param->type) === '') {                        $param->type = $inheritedMethod->params[$i]->type;                    }                    if (empty($param->types)) {                        $param->types = $inheritedMethod->params[$i]->types;                    }                }                $m->removeTag('inheritdoc');            }        }    }    /**     * @param MethodDoc $method     * @param ClassDoc $class     * @return mixed     */    private function inheritMethodRecursive($method, $class)    {        $inheritanceCandidates = array_merge(            $this->getParents($class),            $this->getInterfaces($class)        );        $methods = [];        foreach($inheritanceCandidates as $candidate) {            if (isset($candidate->methods[$method->name])) {                $cmethod = $candidate->methods[$method->name];                if ($cmethod->hasTag('inheritdoc')) {                    $this->inheritDocs($candidate);                }                $methods[] = $cmethod;            }        }        return reset($methods);    }    /**     * @param PropertyDoc $method     * @param ClassDoc $class     * @return mixed     */    private function inheritPropertyRecursive($method, $class)    {        $inheritanceCandidates = array_merge(            $this->getParents($class),            $this->getInterfaces($class)        );        $properties = [];        foreach($inheritanceCandidates as $candidate) {            if (isset($candidate->properties[$method->name])) {                $cproperty = $candidate->properties[$method->name];                if ($cproperty->hasTag('inheritdoc')) {                    $this->inheritDocs($candidate);                }                $properties[] = $cproperty;            }        }        return reset($properties);    }    /**     * @param ClassDoc $class     * @return array     */    private function getParents($class)    {        if ($class->parentClass === null || !isset($this->classes[$class->parentClass])) {            return [];        }        return array_merge([$this->classes[$class->parentClass]], $this->getParents($this->classes[$class->parentClass]));    }    /**     * @param ClassDoc $class     * @return array     */    private function getInterfaces($class)    {        $interfaces = [];        foreach($class->interfaces as $interface) {            if (isset($this->interfaces[$interface])) {                $interfaces[] = $this->interfaces[$interface];            }        }        return $interfaces;    }    /**     * Add properties for getters and setters if class is subclass of [[\yii\base\Object]].     * @param ClassDoc $class     */    protected function handlePropertyFeature($class)    {        if (!$this->isSubclassOf($class, 'yii\base\Object')) {            return;        }        foreach ($class->getPublicMethods() as $name => $method) {            if ($method->isStatic) {                continue;            }            if (!strncmp($name, 'get', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method)) {                $propertyName = '$' . lcfirst(substr($method->name, 3));                if (isset($class->properties[$propertyName])) {                    $property = $class->properties[$propertyName];                    if ($property->getter === null && $property->setter === null) {                        $this->errors[] = [                            'line' => $property->startLine,                            'file' => $class->sourceFile,                            'message' => "Property $propertyName conflicts with a defined getter {$method->name} in {$class->name}.",                        ];                    }                    $property->getter = $method;                } else {                    $class->properties[$propertyName] = new PropertyDoc(null, $this, [                        'name' => $propertyName,                        'definedBy' => $method->definedBy,                        'sourceFile' => $class->sourceFile,                        'visibility' => 'public',                        'isStatic' => false,                        'type' => $method->returnType,                        'types' => $method->returnTypes,                        'shortDescription' => BaseDoc::extractFirstSentence($method->return),                        'description' => $method->return,                        'getter' => $method                        // TODO set default value                    ]);                }            }            if (!strncmp($name, 'set', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method, 1)) {                $propertyName = '$' . lcfirst(substr($method->name, 3));                if (isset($class->properties[$propertyName])) {                    $property = $class->properties[$propertyName];                    if ($property->getter === null && $property->setter === null) {                        $this->errors[] = [                            'line' => $property->startLine,                            'file' => $class->sourceFile,                            'message' => "Property $propertyName conflicts with a defined setter {$method->name} in {$class->name}.",                        ];                    }                    $property->setter = $method;                } else {                    $param = $this->getFirstNotOptionalParameter($method);                    $class->properties[$propertyName] = new PropertyDoc(null, $this, [                        'name' => $propertyName,                        'definedBy' => $method->definedBy,                        'sourceFile' => $class->sourceFile,                        'visibility' => 'public',                        'isStatic' => false,                        'type' => $param->type,                        'types' => $param->types,                        'shortDescription' => BaseDoc::extractFirstSentence($param->description),                        'description' => $param->description,                        'setter' => $method                    ]);                }            }        }    }    /**     * Check whether a method has `$number` non-optional parameters.     * @param MethodDoc $method     * @param int $number number of not optional parameters     * @return bool     */    private function hasNonOptionalParams($method, $number = 0)    {        $count = 0;        foreach ($method->params as $param) {            if (!$param->isOptional) {                $count++;            }        }        return $count == $number;    }    /**     * @param MethodDoc $method     * @return ParamDoc     */    private function getFirstNotOptionalParameter($method)    {        foreach ($method->params as $param) {            if (!$param->isOptional) {                return $param;            }        }        return null;    }    /**     * @param ClassDoc $classA     * @param ClassDoc|string $classB     * @return bool     */    protected function isSubclassOf($classA, $classB)    {        if (is_object($classB)) {            $classB = $classB->name;        }        if ($classA->name == $classB) {            return true;        }        while ($classA->parentClass !== null && isset($this->classes[$classA->parentClass])) {            $classA = $this->classes[$classA->parentClass];            if ($classA->name == $classB) {                return true;            }        }        return false;    }}
 |