Context.php 14 KB
Newer Older
1 2
<?php
/**
3 4 5
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
6 7
 */

8
namespace yii\apidoc\models;
9

10
use phpDocumentor\Reflection\FileReflector;
11 12
use yii\base\Component;

13 14 15 16 17
/**
 *
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
18 19
class Context extends Component
{
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
    /**
     * @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 = [];
    public $errors = [];
37

38

39 40 41 42 43 44 45 46 47 48
    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];
        }
49

50 51
        return null;
    }
52

53 54 55
    public function addFile($fileName)
    {
        $this->files[$fileName] = sha1_file($fileName);
56

57 58
        $reflection = new FileReflector($fileName, true);
        $reflection->process();
59

60 61 62 63 64 65 66 67 68 69 70 71 72
        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;
        }
    }
73

74 75 76 77 78 79 80 81 82 83 84 85
    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) {
86
            $this->updateSubclassInterfacesTraits($class);
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
        }
        // 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;
                            }
                        }
                    }
                }
            }
        }
        // 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);
        }
        // add properties from getters and setters
        foreach ($this->classes as $class) {
            $this->handlePropertyFeature($class);
        }
124

125 126
        // TODO reference exceptions to methods where they are thrown
    }
127

128 129 130 131
    /**
     * Add implemented interfaces and used traits to subclasses
     * @param ClassDoc $class
     */
132
    protected function updateSubclassInterfacesTraits($class)
133 134 135 136 137
    {
        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));
138
            $this->updateSubclassInterfacesTraits($subclass);
139 140
        }
    }
141

142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
    /**
     * 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);
        }
    }
157

158 159 160 161 162 163 164 165 166
    /**
     * Inhertit docsblocks using `@inheritDoc` tag.
     * @param ClassDoc $class
     * @see http://phpdoc.org/docs/latest/guides/inheritance.html
     */
    protected function inheritDocs($class)
    {
        // TODO also for properties?
        foreach ($class->methods as $m) {
167 168
            if ($m->hasTag('inheritdoc')) {
                $inheritedMethod = $this->inheritMethodRecursive($m, $class);
Carsten Brandt committed
169 170 171 172 173 174 175 176
                if (!$inheritedMethod) {
                    $this->errors[] = [
                        'line' => $m->startLine,
                        'file' => $class->sourceFile,
                        'message' => "Method {$m->name} has no parent to inherit from in {$class->name}.",
                    ];
                    continue;
                }
177
                foreach (['shortDescription', 'description', 'return', 'returnType', 'returnTypes', 'exceptions'] as $property) {
178
                    // set all properties that are empty. descriptions will be concatenated.
179 180
                    if (empty($m->$property) || is_string($m->$property) && trim($m->$property) === '') {
                        $m->$property = $inheritedMethod->$property;
181 182
                    } elseif ($property == 'description') {
                        $m->$property = rtrim($m->$property) . "\n\n" . ltrim($inheritedMethod->$property);
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
                    }
                }
                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;
                    }
200
                    if (empty($param->types)) {
201 202
                        $param->types = $inheritedMethod->params[$i]->types;
                    }
203
                }
204
                $m->removeTag('inheritdoc');
205 206 207
            }
        }
    }
208

209 210
    /**
     * @param MethodDoc $method
211
     * @param ClassDoc $parent
212 213 214
     */
    private function inheritMethodRecursive($method, $class)
    {
215 216 217 218 219 220 221 222 223 224 225
        $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);
226
                }
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
                $methods[] = $cmethod;
            }
        }

        return reset($methods);
    }

    /**
     * @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]));
    }
245

246 247 248 249 250 251 252 253 254 255
    /**
     * @param ClassDoc $class
     * @return array
     */
    private function getInterfaces($class)
    {
        $interfaces = [];
        foreach($class->interfaces as $interface) {
            if (isset($this->interfaces[$interface])) {
                $interfaces[] = $this->interfaces[$interface];
256 257
            }
        }
258
        return $interfaces;
259
    }
260

261 262 263 264 265 266 267 268 269 270 271 272 273
    /**
     * 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;
            }
274
            if (!strncmp($name, 'get', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method)) {
275 276 277 278 279 280 281 282 283 284 285 286 287 288
                $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,
289
                        'definedBy' => $method->definedBy,
290 291 292 293 294
                        'sourceFile' => $class->sourceFile,
                        'visibility' => 'public',
                        'isStatic' => false,
                        'type' => $method->returnType,
                        'types' => $method->returnTypes,
295
                        'shortDescription' => BaseDoc::extractFirstSentence($method->return),
296 297 298 299 300 301
                        'description' => $method->return,
                        'getter' => $method
                        // TODO set default value
                    ]);
                }
            }
302
            if (!strncmp($name, 'set', 3) && strlen($name) > 3 && $this->hasNonOptionalParams($method, 1)) {
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
                $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,
318
                        'definedBy' => $method->definedBy,
319 320 321 322 323
                        'sourceFile' => $class->sourceFile,
                        'visibility' => 'public',
                        'isStatic' => false,
                        'type' => $param->type,
                        'types' => $param->types,
324
                        'shortDescription' => BaseDoc::extractFirstSentence($param->description),
325 326 327 328 329 330 331
                        'description' => $param->description,
                        'setter' => $method
                    ]);
                }
            }
        }
    }
332

333
    /**
334
     * Check whether a method has `$number` non-optional parameters.
335 336
     * @param MethodDoc $method
     * @param integer $number number of not optional parameters
337 338
     * @return bool
     */
339
    private function hasNonOptionalParams($method, $number = 0)
340
    {
341
        $count = 0;
342
        foreach ($method->params as $param) {
343 344
            if (!$param->isOptional) {
                $count++;
345 346
            }
        }
347
        return $count == $number;
348 349 350
    }

    /**
351
     * @param MethodDoc $method
352 353 354 355 356 357 358 359 360 361 362 363 364
     * @return ParamDoc
     */
    private function getFirstNotOptionalParameter($method)
    {
        foreach ($method->params as $param) {
            if (!$param->isOptional) {
                return $param;
            }
        }
        return null;
    }

    /**
365 366
     * @param ClassDoc $classA
     * @param ClassDoc|string $classB
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
     * @return boolean
     */
    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;
    }
AlexGx committed
385
}